Add comprehensive setup and validation system for Umami - Create interactive setup wizard - Add setup validation script - Enhance error messages - Create SETUP.md documentation - Add .env.example template - Implement pre-flight checks - Add TypeScript types - Create tests - Update README

This commit is contained in:
Ayush3603 2025-11-10 22:19:51 +05:30
parent 06422fb65f
commit 8ccaf3dcb0
15 changed files with 30895 additions and 7 deletions

View file

@ -0,0 +1,207 @@
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = join(__dirname, '../..');
describe('Setup Integration Tests', () => {
describe('Complete Setup Flow', () => {
it('should validate setup with all checks', () => {
try {
const output = execSync('node scripts/setup-validator.js', {
cwd: projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
});
expect(output).toContain('Validating Umami Setup');
expect(output).toMatch(/Node\.js Version/);
expect(output).toMatch(/Package Manager/);
} catch (error) {
// Test passes if script runs (even with failures)
expect(error.stdout || error.stderr).toBeDefined();
}
});
it('should handle missing .env file gracefully', () => {
const envPath = join(projectRoot, '.env');
const envExists = existsSync(envPath);
if (envExists) {
// Skip if .env exists (don't want to break actual setup)
expect(true).toBe(true);
return;
}
try {
execSync('node scripts/setup-validator.js', {
cwd: projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
});
} catch (error) {
const output = error.stdout || error.stderr || '';
expect(output).toContain('Environment Configuration');
}
});
});
describe('Error Recovery', () => {
it('should provide helpful error messages', () => {
try {
execSync('node scripts/check-env.js', {
cwd: projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
env: { ...process.env, DATABASE_URL: undefined },
});
} catch (error) {
const output = error.stdout || '';
// Should contain helpful information
expect(output.length).toBeGreaterThan(0);
}
});
it('should exit with non-zero code on validation failure', () => {
try {
execSync('node scripts/check-env.js', {
cwd: projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
env: { ...process.env, DATABASE_URL: undefined, SKIP_DB_CHECK: undefined },
});
// Should not reach here
expect(false).toBe(true);
} catch (error) {
expect(error.status).not.toBe(0);
}
});
});
describe('Build Process Integration', () => {
it('should have check-env in build script', () => {
const packageJson = JSON.parse(
execSync('cat package.json', {
cwd: projectRoot,
encoding: 'utf-8',
}),
);
expect(packageJson.scripts.build).toContain('check-env');
});
it('should have validate-setup script', () => {
const packageJson = JSON.parse(
execSync('cat package.json', {
cwd: projectRoot,
encoding: 'utf-8',
}),
);
expect(packageJson.scripts['validate-setup']).toBeDefined();
expect(packageJson.scripts['validate-setup']).toContain('setup-validator');
});
it('should have setup wizard script', () => {
const packageJson = JSON.parse(
execSync('cat package.json', {
cwd: projectRoot,
encoding: 'utf-8',
}),
);
expect(packageJson.scripts.setup).toBeDefined();
expect(packageJson.scripts.setup).toContain('quick-setup');
});
});
describe('Script Execution', () => {
it('should execute setup-validator without errors', () => {
try {
execSync('node scripts/setup-validator.js', {
cwd: projectRoot,
stdio: 'pipe',
});
expect(true).toBe(true);
} catch (error) {
// Script may fail validation but should execute
expect(error.status).toBeDefined();
}
});
it('should execute check-env without syntax errors', () => {
try {
execSync('node scripts/check-env.js', {
cwd: projectRoot,
stdio: 'pipe',
});
expect(true).toBe(true);
} catch (error) {
// Script may fail validation but should execute
expect(error.status).toBeDefined();
}
});
});
describe('Documentation Files', () => {
it('should have SETUP.md file', () => {
const setupMdPath = join(projectRoot, 'SETUP.md');
expect(existsSync(setupMdPath)).toBe(true);
});
it('should have .env.example file', () => {
const envExamplePath = join(projectRoot, '.env.example');
expect(existsSync(envExamplePath)).toBe(true);
});
it('SETUP.md should contain key sections', () => {
const setupMdPath = join(projectRoot, 'SETUP.md');
if (existsSync(setupMdPath)) {
const content = execSync(`cat "${setupMdPath}"`, {
cwd: projectRoot,
encoding: 'utf-8',
});
expect(content).toContain('Prerequisites');
expect(content).toContain('Installation');
expect(content).toContain('Database');
expect(content).toContain('Troubleshooting');
}
});
it('.env.example should contain DATABASE_URL', () => {
const envExamplePath = join(projectRoot, '.env.example');
if (existsSync(envExamplePath)) {
const content = execSync(`cat "${envExamplePath}"`, {
cwd: projectRoot,
encoding: 'utf-8',
});
expect(content).toContain('DATABASE_URL');
expect(content).toContain('postgresql://');
}
});
});
describe('Validation Scripts Exist', () => {
const scripts = [
'setup-validator.js',
'quick-setup.js',
'check-env.js',
'check-db.js',
'pre-dev.js',
'pre-start.js',
];
scripts.forEach(script => {
it(`should have ${script}`, () => {
const scriptPath = join(projectRoot, 'scripts', script);
expect(existsSync(scriptPath)).toBe(true);
});
});
});
});

View file

@ -0,0 +1,184 @@
import {
checkNodeVersion,
checkPackageManager,
checkEnvFile,
checkDatabaseUrl,
checkDependencies,
} from '../setup-validator.js';
describe('Setup Validator', () => {
describe('checkNodeVersion', () => {
it('should pass when Node.js version is >= 18.18', async () => {
const result = await checkNodeVersion();
expect(result.check).toBe('Node.js Version');
expect(['pass', 'fail']).toContain(result.status);
expect(result.message).toBeDefined();
});
it('should have proper structure', async () => {
const result = await checkNodeVersion();
expect(result).toHaveProperty('check');
expect(result).toHaveProperty('status');
expect(result).toHaveProperty('message');
if (result.status === 'fail') {
expect(result).toHaveProperty('solution');
expect(result).toHaveProperty('documentation');
}
});
});
describe('checkPackageManager', () => {
it('should check if pnpm is installed', async () => {
const result = await checkPackageManager();
expect(result.check).toBe('Package Manager (pnpm)');
expect(['pass', 'fail']).toContain(result.status);
expect(result.message).toBeDefined();
});
it('should provide solution when pnpm is not found', async () => {
const result = await checkPackageManager();
if (result.status === 'fail') {
expect(result.solution).toContain('pnpm');
expect(result.documentation).toBeDefined();
}
});
});
describe('checkEnvFile', () => {
it('should check for .env file existence', async () => {
const result = await checkEnvFile();
expect(result.check).toBe('Environment Configuration');
expect(['pass', 'fail']).toContain(result.status);
expect(result.message).toBeDefined();
});
it('should provide solution when .env is missing', async () => {
const result = await checkEnvFile();
if (result.status === 'fail') {
expect(result.solution).toBeDefined();
}
});
});
describe('checkDatabaseUrl', () => {
const originalEnv = process.env.DATABASE_URL;
afterEach(() => {
process.env.DATABASE_URL = originalEnv;
});
it('should fail when DATABASE_URL is not set', async () => {
delete process.env.DATABASE_URL;
const result = await checkDatabaseUrl();
expect(result.check).toBe('Database URL Format');
expect(result.status).toBe('fail');
expect(result.message).toContain('DATABASE_URL');
});
it('should fail when DATABASE_URL format is invalid', async () => {
process.env.DATABASE_URL = 'invalid-url';
const result = await checkDatabaseUrl();
expect(result.status).toBe('fail');
expect(result.message).toContain('format');
expect(result.solution).toBeDefined();
});
it('should pass when DATABASE_URL format is valid', async () => {
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db';
const result = await checkDatabaseUrl();
expect(result.status).toBe('pass');
expect(result.message).toContain('valid');
});
it('should validate PostgreSQL URL pattern', async () => {
const validUrls = [
'postgresql://user:pass@localhost:5432/db',
'postgresql://admin:secret@192.168.1.1:5432/umami',
'postgresql://test:test123@db.example.com:5432/analytics',
];
for (const url of validUrls) {
process.env.DATABASE_URL = url;
const result = await checkDatabaseUrl();
expect(result.status).toBe('pass');
}
});
it('should reject invalid URL patterns', async () => {
const invalidUrls = [
'mysql://user:pass@localhost:3306/db',
'http://localhost:5432',
'postgresql://localhost',
'user:pass@localhost:5432/db',
];
for (const url of invalidUrls) {
process.env.DATABASE_URL = url;
const result = await checkDatabaseUrl();
expect(result.status).toBe('fail');
}
});
});
describe('checkDependencies', () => {
it('should check for node_modules directory', async () => {
const result = await checkDependencies();
expect(result.check).toBe('Dependencies');
expect(['pass', 'fail']).toContain(result.status);
expect(result.message).toBeDefined();
});
it('should provide solution when dependencies are missing', async () => {
const result = await checkDependencies();
if (result.status === 'fail') {
expect(result.solution).toContain('pnpm install');
}
});
});
describe('Validation Result Structure', () => {
it('all checks should return consistent structure', async () => {
const checks = [
checkNodeVersion,
checkPackageManager,
checkEnvFile,
checkDatabaseUrl,
checkDependencies,
];
for (const check of checks) {
const result = await check();
expect(result).toHaveProperty('check');
expect(result).toHaveProperty('status');
expect(result).toHaveProperty('message');
expect(typeof result.check).toBe('string');
expect(['pass', 'fail', 'warning']).toContain(result.status);
expect(typeof result.message).toBe('string');
if (result.solution) {
expect(typeof result.solution).toBe('string');
}
if (result.documentation) {
expect(typeof result.documentation).toBe('string');
}
}
});
});
});

36
scripts/dev-server.js Normal file
View file

@ -0,0 +1,36 @@
/* eslint-disable no-console */
import { spawn } from 'node:child_process';
import chalk from 'chalk';
console.log(chalk.bold.cyan('\n🚀 Starting Umami Development Server...\n'));
// Start Next.js dev server
const devServer = spawn('next', ['dev', '-p', '3001', '--turbo'], {
stdio: 'inherit',
shell: true,
});
devServer.on('spawn', () => {
setTimeout(() => {
console.log(chalk.green.bold('\n✅ Development server is running!\n'));
console.log(chalk.cyan('📍 Local: http://localhost:3001'));
console.log(chalk.cyan('📍 Network: Use your local IP address\n'));
console.log(chalk.gray('Features enabled:'));
console.log(chalk.gray(' • Hot Module Replacement (HMR)'));
console.log(chalk.gray(' • Turbo Mode for faster builds'));
console.log(chalk.gray(' • Detailed error messages\n'));
console.log(chalk.yellow('💡 Press Ctrl+C to stop the server\n'));
}, 2000);
});
devServer.on('error', error => {
console.error(chalk.red.bold('\n❌ Failed to start development server:'), error.message);
process.exit(1);
});
devServer.on('exit', code => {
if (code !== 0 && code !== null) {
console.error(chalk.red.bold(`\n❌ Development server exited with code ${code}\n`));
process.exit(code);
}
});

26
scripts/pre-dev.js Normal file
View file

@ -0,0 +1,26 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import { validateSetup } from './setup-validator.js';
console.log(chalk.bold.cyan('\n🚀 Starting Umami Development Server...\n'));
try {
const result = await validateSetup();
if (result.overall === 'error') {
console.log(chalk.red.bold('\n❌ Cannot start development server due to validation errors.\n'));
console.log(chalk.cyan('Please fix the errors above and try again.\n'));
process.exit(1);
}
if (result.overall === 'incomplete') {
console.log(
chalk.yellow.bold('\n⚠ Starting with warnings. Some features may not work correctly.\n'),
);
}
console.log(chalk.green.bold('✅ Validation passed! Starting Next.js development server...\n'));
} catch (error) {
console.error(chalk.red.bold('\n❌ Pre-flight check failed:'), error.message);
process.exit(1);
}

51
scripts/pre-start.js Normal file
View file

@ -0,0 +1,51 @@
/* eslint-disable no-console */
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import chalk from 'chalk';
import 'dotenv/config';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = join(__dirname, '..');
console.log(chalk.bold.cyan('\n🚀 Starting Umami Production Server...\n'));
// Check if build exists
const nextBuildPath = join(projectRoot, '.next');
if (!existsSync(nextBuildPath)) {
console.log(chalk.red.bold('❌ Build artifacts not found!\n'));
console.log(chalk.yellow('💡 Solution:'));
console.log(chalk.cyan(' Run the build command first: pnpm run build\n'));
process.exit(1);
}
console.log(chalk.green('✓ Build artifacts found'));
// Check environment variables
if (!process.env.DATABASE_URL) {
console.log(chalk.red.bold('\n❌ DATABASE_URL is not set!\n'));
console.log(chalk.yellow('💡 Solution:'));
console.log(chalk.cyan(' Ensure .env file exists with DATABASE_URL configured\n'));
process.exit(1);
}
console.log(chalk.green('✓ Environment variables configured'));
// Check database connectivity (optional, can be skipped)
if (!process.env.SKIP_DB_CHECK) {
try {
const { execSync } = await import('node:child_process');
execSync('node scripts/check-db.js', { stdio: 'pipe' });
console.log(chalk.green('✓ Database connection verified'));
} catch (err) {
console.log(chalk.red('✗ Database connection failed'));
console.log(chalk.gray(`Error: ${err.message}`));
console.log(chalk.yellow('\n⚠ Warning: Database is not accessible'));
console.log(chalk.gray('The server will start but may not function correctly.\n'));
}
}
console.log(chalk.green.bold('\n✅ Pre-start validation passed!\n'));
console.log(chalk.cyan('Starting Next.js production server...\n'));

48
scripts/prod-server.js Normal file
View file

@ -0,0 +1,48 @@
/* eslint-disable no-console */
import { spawn } from 'node:child_process';
import chalk from 'chalk';
import 'dotenv/config';
console.log(chalk.bold.cyan('\n🚀 Starting Umami Production Server...\n'));
// Start Next.js production server
const prodServer = spawn('next', ['start'], {
stdio: 'inherit',
shell: true,
});
prodServer.on('spawn', () => {
setTimeout(() => {
console.log(chalk.green.bold('\n✅ Production server is running!\n'));
console.log(chalk.cyan('📍 Local: http://localhost:3000'));
console.log(chalk.cyan('📍 Network: Use your local IP address\n'));
console.log(chalk.gray('Environment:'));
console.log(chalk.gray(` • Mode: Production`));
console.log(chalk.gray(` • Database: Connected`));
if (process.env.BASE_PATH) {
console.log(chalk.gray(` • Base Path: ${process.env.BASE_PATH}`));
}
console.log('');
console.log(chalk.yellow.bold('⚠️ Security Reminders:\n'));
console.log(chalk.yellow(' 1. Change default admin password (admin/umami)'));
console.log(chalk.yellow(' 2. Use HTTPS in production (set FORCE_SSL=1)'));
console.log(chalk.yellow(' 3. Keep your DATABASE_URL secure'));
console.log(chalk.yellow(' 4. Regularly update dependencies\n'));
console.log(chalk.gray('💡 Press Ctrl+C to stop the server\n'));
}, 2000);
});
prodServer.on('error', error => {
console.error(chalk.red.bold('\n❌ Failed to start production server:'), error.message);
process.exit(1);
});
prodServer.on('exit', code => {
if (code !== 0 && code !== null) {
console.error(chalk.red.bold(`\n❌ Production server exited with code ${code}\n`));
process.exit(code);
}
});

405
scripts/quick-setup.js Normal file
View file

@ -0,0 +1,405 @@
/* eslint-disable no-console */
import { execSync } from 'node:child_process';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import chalk from 'chalk';
import prompts from 'prompts';
import { checkNodeVersion, checkPackageManager } from './setup-validator.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = join(__dirname, '..');
/**
* Display welcome message
*/
function displayWelcome() {
console.clear();
console.log(chalk.bold.cyan('\n╔════════════════════════════════════════╗'));
console.log(chalk.bold.cyan('║ ║'));
console.log(chalk.bold.cyan('║ Welcome to Umami Setup Wizard ║'));
console.log(chalk.bold.cyan('║ ║'));
console.log(chalk.bold.cyan('╚════════════════════════════════════════╝\n'));
console.log(
chalk.gray('This wizard will guide you through setting up Umami for local development.\n'),
);
}
/**
* Main setup function
*/
async function quickSetup() {
displayWelcome();
try {
// Step 1: Check prerequisites
console.log(chalk.bold.yellow('Step 1: Checking Prerequisites\n'));
await checkPrerequisites();
// Step 2: Configure environment
console.log(chalk.bold.yellow('\nStep 2: Database Configuration\n'));
const dbConfig = await promptDatabaseConfig();
// Step 3: Create .env file
console.log(chalk.bold.yellow('\nStep 3: Creating Environment File\n'));
await createEnvFile(dbConfig);
// Step 4: Install dependencies
console.log(chalk.bold.yellow('\nStep 4: Installing Dependencies\n'));
const shouldInstall = await promptInstallDependencies();
if (shouldInstall) {
await installDependencies();
}
// Step 5: Validate database
console.log(chalk.bold.yellow('\nStep 5: Validating Database\n'));
await validateDatabase();
// Step 6: Build application
console.log(chalk.bold.yellow('\nStep 6: Building Application\n'));
const shouldBuild = await promptBuild();
if (shouldBuild) {
await runBuild();
}
// Step 7: Display completion summary
displayCompletionSummary(shouldBuild);
} catch (error) {
if (error.message === 'Setup cancelled') {
console.log(chalk.yellow('\n⚠ Setup cancelled by user.\n'));
process.exit(0);
}
console.error(chalk.red.bold('\n❌ Setup failed:'), error.message);
console.log(chalk.cyan('\nPlease fix the error and run the setup wizard again.\n'));
process.exit(1);
}
}
/**
* Prompt for database configuration
*/
async function promptDatabaseConfig() {
console.log(chalk.gray('Please provide your PostgreSQL database connection details:\n'));
const response = await prompts(
[
{
type: 'text',
name: 'host',
message: 'Database host:',
initial: 'localhost',
},
{
type: 'number',
name: 'port',
message: 'Database port:',
initial: 5432,
},
{
type: 'text',
name: 'database',
message: 'Database name:',
initial: 'umami',
},
{
type: 'text',
name: 'username',
message: 'Database username:',
initial: 'postgres',
},
{
type: 'password',
name: 'password',
message: 'Database password:',
},
],
{
onCancel: () => {
throw new Error('Setup cancelled');
},
},
);
// Construct DATABASE_URL
const databaseUrl = `postgresql://${response.username}:${response.password}@${response.host}:${response.port}/${response.database}`;
console.log(chalk.cyan('\n📝 Connection string:'));
// Mask password in display
const maskedUrl = databaseUrl.replace(/:([^@]+)@/, ':****@');
console.log(chalk.gray(maskedUrl));
// Test connection
console.log(chalk.cyan('\n🔍 Testing database connection...'));
try {
// Set temporary env var for testing
process.env.DATABASE_URL = databaseUrl;
// Try to connect using psql
execSync(`psql "${databaseUrl}" -c "SELECT version();"`, { stdio: 'pipe' });
console.log(chalk.green('✓ Database connection successful!'));
return { databaseUrl, ...response };
} catch (error) {
console.log(chalk.red('✗ Database connection failed!'));
console.log(chalk.gray(`Error: ${error.message}`));
console.log(chalk.yellow('\n💡 Common issues:'));
console.log(' - PostgreSQL service is not running');
console.log(' - Incorrect username or password');
console.log(' - Database does not exist');
console.log(' - Host or port is incorrect\n');
const retry = await prompts({
type: 'confirm',
name: 'value',
message: 'Would you like to try again?',
initial: true,
});
if (retry.value) {
return promptDatabaseConfig();
} else {
throw new Error('Database connection failed');
}
}
}
/**
* Create .env file with database configuration
*/
async function createEnvFile(dbConfig) {
const envPath = join(projectRoot, '.env');
const envContent = `# Umami Environment Configuration
# Generated by setup wizard on ${new Date().toISOString()}
# Database Configuration (Required)
DATABASE_URL=${dbConfig.databaseUrl}
# Optional Configuration
# Uncomment and configure as needed
# BASE_PATH=/analytics
# TRACKER_SCRIPT_NAME=custom-script.js
# FORCE_SSL=1
# DEFAULT_LOCALE=en-US
# For more options, see .env.example
`;
try {
await writeFile(envPath, envContent, 'utf-8');
console.log(chalk.green('✓ .env file created successfully'));
console.log(chalk.gray(` Location: ${envPath}\n`));
// Confirm with user
const confirm = await prompts({
type: 'confirm',
name: 'value',
message: 'Would you like to add optional configuration now?',
initial: false,
});
if (confirm.value) {
console.log(chalk.cyan('\n📝 You can edit the .env file to add optional configuration.'));
console.log(chalk.gray('See .env.example for all available options.\n'));
}
} catch (err) {
console.log(chalk.red('✗ Failed to create .env file'));
throw new Error(`Failed to create .env file: ${err.message}`);
}
}
/**
* Prompt to install dependencies
*/
async function promptInstallDependencies() {
const response = await prompts({
type: 'confirm',
name: 'value',
message: 'Install dependencies now? (This may take a few minutes)',
initial: true,
});
return response.value;
}
/**
* Install dependencies using pnpm
*/
async function installDependencies() {
console.log(chalk.cyan('\n📦 Installing dependencies...\n'));
try {
execSync('pnpm install', {
cwd: projectRoot,
stdio: 'inherit',
});
console.log(chalk.green('\n✓ Dependencies installed successfully'));
} catch (err) {
console.log(chalk.red('\n✗ Failed to install dependencies'));
console.log(chalk.gray(`Error: ${err.message}`));
throw new Error('Dependency installation failed');
}
}
/**
* Validate database connection
*/
async function validateDatabase() {
console.log(chalk.cyan('🔍 Validating database configuration...\n'));
try {
execSync('node scripts/check-db.js', {
cwd: projectRoot,
stdio: 'inherit',
});
} catch (err) {
console.log(chalk.red('\n✗ Database validation failed'));
console.log(chalk.gray(`Error: ${err.message}`));
throw new Error('Database validation failed');
}
}
/**
* Prompt to build application
*/
async function promptBuild() {
const response = await prompts({
type: 'confirm',
name: 'value',
message: 'Build the application now? (This may take several minutes)',
initial: true,
});
return response.value;
}
/**
* Run build process
*/
async function runBuild() {
console.log(chalk.cyan('\n🔨 Building application...\n'));
console.log(chalk.gray('This will:'));
console.log(chalk.gray(' - Generate Prisma client'));
console.log(chalk.gray(' - Run database migrations'));
console.log(chalk.gray(' - Create database tables'));
console.log(chalk.gray(' - Build tracking script'));
console.log(chalk.gray(' - Build Next.js application\n'));
try {
execSync('pnpm run build', {
cwd: projectRoot,
stdio: 'inherit',
});
console.log(chalk.green('\n✓ Build completed successfully'));
console.log(chalk.yellow('\n⚠ Important: Default admin credentials created:'));
console.log(chalk.cyan(' Username: admin'));
console.log(chalk.cyan(' Password: umami'));
console.log(chalk.yellow(' Please change this password after first login!\n'));
} catch (err) {
console.log(chalk.red('\n✗ Build failed'));
console.log(chalk.gray(`Error: ${err.message}`));
throw new Error('Build process failed');
}
}
/**
* Display completion summary
*/
function displayCompletionSummary(buildCompleted) {
console.log(chalk.bold.green('\n╔════════════════════════════════════════╗'));
console.log(chalk.bold.green('║ ║'));
console.log(chalk.bold.green('║ ✅ Setup Completed Successfully! ║'));
console.log(chalk.bold.green('║ ║'));
console.log(chalk.bold.green('╚════════════════════════════════════════╝\n'));
console.log(chalk.bold.cyan('📋 Next Steps:\n'));
if (buildCompleted) {
console.log(chalk.green('1. Start the development server:'));
console.log(chalk.cyan(' pnpm run dev\n'));
console.log(chalk.green('2. Open your browser and navigate to:'));
console.log(chalk.cyan(' http://localhost:3001\n'));
console.log(chalk.green('3. Log in with default credentials:'));
console.log(chalk.cyan(' Username: admin'));
console.log(chalk.cyan(' Password: umami'));
console.log(chalk.yellow(' ⚠️ Change this password immediately!\n'));
console.log(chalk.green('4. Add your first website and start tracking!\n'));
} else {
console.log(chalk.green('1. Build the application:'));
console.log(chalk.cyan(' pnpm run build\n'));
console.log(chalk.green('2. Start the development server:'));
console.log(chalk.cyan(' pnpm run dev\n'));
console.log(chalk.green('3. Open your browser and navigate to:'));
console.log(chalk.cyan(' http://localhost:3001\n'));
}
console.log(chalk.bold.cyan('📚 Additional Resources:\n'));
console.log(chalk.gray(' • Documentation: https://umami.is/docs'));
console.log(chalk.gray(' • Setup Guide: See SETUP.md in project root'));
console.log(chalk.gray(' • Community: https://umami.is/discord'));
console.log(chalk.gray(' • GitHub: https://github.com/umami-software/umami\n'));
console.log(chalk.bold.cyan('💡 Helpful Commands:\n'));
console.log(chalk.gray(' • Validate setup: node scripts/setup-validator.js'));
console.log(chalk.gray(' • Check database: node scripts/check-db.js'));
console.log(chalk.gray(' • Development mode: pnpm run dev'));
console.log(chalk.gray(' • Production mode: pnpm run start\n'));
console.log(chalk.green('Happy tracking! 🎉\n'));
}
/**
* Check prerequisites (Node.js and pnpm)
*/
async function checkPrerequisites() {
const nodeCheck = await checkNodeVersion();
const pnpmCheck = await checkPackageManager();
console.log(
nodeCheck.status === 'pass'
? chalk.green(`${nodeCheck.message}`)
: chalk.red(`${nodeCheck.message}`),
);
console.log(
pnpmCheck.status === 'pass'
? chalk.green(`${pnpmCheck.message}`)
: chalk.red(`${pnpmCheck.message}`),
);
if (nodeCheck.status === 'fail') {
console.log(chalk.yellow(`\n💡 ${nodeCheck.solution}`));
if (nodeCheck.documentation) {
console.log(chalk.blue(`📖 ${nodeCheck.documentation}`));
}
throw new Error('Node.js version requirement not met');
}
if (pnpmCheck.status === 'fail') {
console.log(chalk.yellow(`\n💡 ${pnpmCheck.solution}`));
if (pnpmCheck.documentation) {
console.log(chalk.blue(`📖 ${pnpmCheck.documentation}`));
}
throw new Error('pnpm not installed');
}
console.log(chalk.green('\n✅ All prerequisites met!'));
}
export { quickSetup };
// Run if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
quickSetup();
}

71
scripts/types/validation.d.ts vendored Normal file
View file

@ -0,0 +1,71 @@
/**
* Validation result structure
*/
export interface ValidationResult {
/** Name of the validation check */
check: string;
/** Status of the check */
status: 'pass' | 'fail' | 'warning';
/** Human-readable message */
message: string;
/** Suggested fix for failures (optional) */
solution?: string;
/** Link to relevant documentation (optional) */
documentation?: string;
}
/**
* Overall setup status
*/
export interface SetupStatus {
/** Overall status */
overall: 'ready' | 'incomplete' | 'error';
/** Number of passed checks */
passed: number;
/** Number of failed checks */
failed: number;
/** Number of warnings */
warnings: number;
/** All validation results */
results: ValidationResult[];
/** Suggested next steps */
nextSteps?: string[];
}
/**
* Environment configuration
*/
export interface EnvironmentConfig {
/** PostgreSQL database connection string (required) */
DATABASE_URL: string;
/** Base path for deployment (optional) */
BASE_PATH?: string;
/** Cloud mode enabled (optional) */
CLOUD_MODE?: string;
/** Cloud URL (optional) */
CLOUD_URL?: string;
/** Tracker script name (optional) */
TRACKER_SCRIPT_NAME?: string;
/** Force SSL (optional) */
FORCE_SSL?: string;
/** Default locale (optional) */
DEFAULT_LOCALE?: string;
/** Allowed frame URLs (optional) */
ALLOWED_FRAME_URLS?: string;
}