mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 05:07:15 +01:00
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:
parent
06422fb65f
commit
8ccaf3dcb0
15 changed files with 30895 additions and 7 deletions
207
scripts/__tests__/setup-integration.test.js
Normal file
207
scripts/__tests__/setup-integration.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
184
scripts/__tests__/setup-validator.test.js
Normal file
184
scripts/__tests__/setup-validator.test.js
Normal 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
36
scripts/dev-server.js
Normal 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
26
scripts/pre-dev.js
Normal 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
51
scripts/pre-start.js
Normal 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
48
scripts/prod-server.js
Normal 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
405
scripts/quick-setup.js
Normal 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
71
scripts/types/validation.d.ts
vendored
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue