mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-02-12 15:52:47 +00:00
597 lines
19 KiB
JavaScript
597 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
|
|
/**
|
|
* Interactive setup script for private prompts.chat clones.
|
|
* Configures branding, theme, authentication, and features.
|
|
*
|
|
* Usage: node scripts/setup.js
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const { execSync, spawn } = require('child_process');
|
|
const p = require('@clack/prompts');
|
|
const color = require('picocolors');
|
|
|
|
const CONFIG_FILE = path.join(__dirname, '..', 'prompts.config.ts');
|
|
const ENV_FILE = path.join(__dirname, '..', '.env');
|
|
|
|
function generateAuthSecret() {
|
|
return crypto.randomBytes(32).toString('base64');
|
|
}
|
|
|
|
function buildDatabaseUrl(config) {
|
|
const { host, port, username, password, database } = config;
|
|
return `postgresql://${username}:${password}@${host}:${port}/${database}`;
|
|
}
|
|
|
|
function generateEnvFile(config) {
|
|
const isLocalhost = config.env.domain.includes('localhost');
|
|
const protocol = isLocalhost ? 'http' : 'https';
|
|
|
|
let envContent = `# Generated by setup script
|
|
# prompts.chat environment configuration
|
|
|
|
# Database
|
|
DATABASE_URL="${config.env.databaseUrl}"
|
|
|
|
# Authentication
|
|
AUTH_SECRET="${config.env.authSecret}"
|
|
AUTH_URL="${protocol}://${config.env.domain}"
|
|
AUTH_TRUST_HOST=true
|
|
|
|
# Cron Job Secret
|
|
CRON_SECRET="${crypto.randomBytes(16).toString('hex')}"
|
|
`;
|
|
|
|
if (config.auth.providers.includes('github')) {
|
|
envContent += `
|
|
# GitHub OAuth (configure at https://github.com/settings/developers)
|
|
AUTH_GITHUB_ID=""
|
|
AUTH_GITHUB_SECRET=""
|
|
`;
|
|
}
|
|
|
|
if (config.auth.providers.includes('google')) {
|
|
envContent += `
|
|
# Google OAuth (configure at https://console.cloud.google.com/apis/credentials)
|
|
AUTH_GOOGLE_ID=""
|
|
AUTH_GOOGLE_SECRET=""
|
|
`;
|
|
}
|
|
|
|
if (config.auth.providers.includes('apple')) {
|
|
envContent += `
|
|
# Apple Sign In
|
|
AUTH_APPLE_ID=""
|
|
AUTH_APPLE_SECRET=""
|
|
`;
|
|
}
|
|
|
|
if (config.auth.providers.includes('azure')) {
|
|
envContent += `
|
|
# Microsoft Azure AD
|
|
AUTH_AZURE_AD_CLIENT_ID=""
|
|
AUTH_AZURE_AD_CLIENT_SECRET=""
|
|
AUTH_AZURE_AD_ISSUER=""
|
|
`;
|
|
}
|
|
|
|
if (config.features.aiSearch || config.features.aiGeneration) {
|
|
envContent += `
|
|
# AI Features
|
|
OPENAI_API_KEY=""
|
|
# OPENAI_BASE_URL=https://api.openai.com/v1
|
|
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
|
# OPENAI_GENERATIVE_MODEL=gpt-4o-mini
|
|
`;
|
|
}
|
|
|
|
return envContent;
|
|
}
|
|
|
|
function generateConfig(config) {
|
|
const sponsorsSection = config.sponsors.length > 0
|
|
? `[
|
|
${config.sponsors.map(s => `{ name: "${s.name}", logo: "${s.logo}", url: "${s.url}" }`).join(',\n ')}
|
|
]`
|
|
: '[]';
|
|
|
|
return `import { defineConfig } from "@/lib/config";
|
|
|
|
// Private clone configuration - generated by setup script
|
|
const useCloneBranding = true;
|
|
|
|
export default defineConfig({
|
|
// Branding - your organization's identity
|
|
branding: {
|
|
name: "${config.branding.name}",
|
|
logo: "${config.branding.logo}",
|
|
logoDark: "${config.branding.logoDark}",
|
|
favicon: "${config.branding.favicon}",
|
|
description: "${config.branding.description}",
|
|
},
|
|
|
|
// Theme - design system configuration
|
|
theme: {
|
|
radius: "${config.theme.radius}",
|
|
variant: "${config.theme.variant}",
|
|
density: "${config.theme.density}",
|
|
colors: {
|
|
primary: "${config.theme.primaryColor}",
|
|
},
|
|
},
|
|
|
|
// Authentication plugins
|
|
auth: {
|
|
providers: [${config.auth.providers.map(provider => `"${provider}"`).join(', ')}],
|
|
allowRegistration: ${config.auth.allowRegistration},
|
|
},
|
|
|
|
// Internationalization
|
|
i18n: {
|
|
locales: [${config.i18n.locales.map(l => `"${l}"`).join(', ')}],
|
|
defaultLocale: "${config.i18n.defaultLocale}",
|
|
},
|
|
|
|
// Features
|
|
features: {
|
|
privatePrompts: ${config.features.privatePrompts},
|
|
changeRequests: ${config.features.changeRequests},
|
|
categories: ${config.features.categories},
|
|
tags: ${config.features.tags},
|
|
comments: ${config.features.comments},
|
|
aiSearch: ${config.features.aiSearch},
|
|
aiGeneration: ${config.features.aiGeneration},
|
|
mcp: ${config.features.mcp},
|
|
},
|
|
|
|
// Homepage customization (clone branding mode)
|
|
homepage: {
|
|
useCloneBranding,
|
|
achievements: {
|
|
enabled: false, // Disabled for private clones
|
|
},
|
|
sponsors: {
|
|
enabled: ${config.sponsors.length > 0},
|
|
items: ${sponsorsSection},
|
|
},
|
|
},
|
|
});
|
|
`;
|
|
}
|
|
|
|
function handleCancel() {
|
|
p.cancel('Setup cancelled.');
|
|
process.exit(0);
|
|
}
|
|
|
|
async function main() {
|
|
console.clear();
|
|
|
|
p.intro(color.bgCyan(color.black(' prompts.chat - Private Clone Setup ')));
|
|
|
|
const config = {
|
|
env: {},
|
|
branding: {},
|
|
theme: {},
|
|
auth: {},
|
|
i18n: {},
|
|
features: {},
|
|
sponsors: []
|
|
};
|
|
|
|
// === ENVIRONMENT ===
|
|
p.log.step(color.cyan('Environment Configuration'));
|
|
|
|
const envConfig = await p.group({
|
|
domain: () => p.text({
|
|
message: 'Application domain',
|
|
placeholder: 'localhost:3000',
|
|
defaultValue: 'localhost:3000',
|
|
}),
|
|
}, { onCancel: handleCancel });
|
|
|
|
config.env.domain = envConfig.domain;
|
|
config.env.authSecret = generateAuthSecret();
|
|
|
|
// === DATABASE ===
|
|
p.log.step(color.cyan('Database Configuration'));
|
|
|
|
const dbConfig = await p.group({
|
|
host: () => p.text({
|
|
message: 'Database host',
|
|
placeholder: 'localhost',
|
|
defaultValue: 'localhost',
|
|
}),
|
|
port: () => p.text({
|
|
message: 'Database port',
|
|
placeholder: '5432',
|
|
defaultValue: '5432',
|
|
}),
|
|
database: () => p.text({
|
|
message: 'Database name',
|
|
placeholder: 'prompts',
|
|
defaultValue: 'prompts',
|
|
}),
|
|
username: () => p.text({
|
|
message: 'Database username',
|
|
placeholder: 'postgres',
|
|
defaultValue: 'postgres',
|
|
}),
|
|
password: () => p.password({
|
|
message: 'Database password',
|
|
mask: '*',
|
|
}),
|
|
}, { onCancel: handleCancel });
|
|
|
|
config.env.database = dbConfig;
|
|
config.env.databaseUrl = buildDatabaseUrl(dbConfig);
|
|
|
|
// === BRANDING ===
|
|
p.log.step(color.cyan('Branding'));
|
|
|
|
const branding = await p.group({
|
|
name: () => p.text({
|
|
message: 'Organization/App name',
|
|
placeholder: 'My Prompt Library',
|
|
defaultValue: 'My Prompt Library',
|
|
}),
|
|
description: () => p.text({
|
|
message: 'Description',
|
|
placeholder: 'Collect, organize, and share AI prompts',
|
|
defaultValue: 'Collect, organize, and share AI prompts',
|
|
}),
|
|
logo: () => p.text({
|
|
message: 'Logo path (public folder)',
|
|
placeholder: '/logo.svg',
|
|
defaultValue: '/logo.svg',
|
|
}),
|
|
logoDark: ({ results }) => p.text({
|
|
message: 'Dark mode logo path',
|
|
placeholder: results.logo || '/logo.svg',
|
|
defaultValue: results.logo || '/logo.svg',
|
|
}),
|
|
favicon: () => p.text({
|
|
message: 'Favicon path',
|
|
placeholder: '/logo.svg',
|
|
defaultValue: '/logo.svg',
|
|
}),
|
|
}, { onCancel: handleCancel });
|
|
|
|
config.branding = branding;
|
|
|
|
// === THEME ===
|
|
p.log.step(color.cyan('Theme'));
|
|
|
|
const theme = await p.group({
|
|
primaryColor: () => p.text({
|
|
message: 'Primary color (hex)',
|
|
placeholder: '#6366f1',
|
|
defaultValue: '#6366f1',
|
|
validate: (value) => {
|
|
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
return 'Please enter a valid hex color (e.g., #6366f1)';
|
|
}
|
|
},
|
|
}),
|
|
radius: () => p.select({
|
|
message: 'Border radius style',
|
|
initialValue: 'sm',
|
|
options: [
|
|
{ value: 'none', label: 'None', hint: 'Sharp corners' },
|
|
{ value: 'sm', label: 'Small', hint: 'Subtle rounding' },
|
|
{ value: 'md', label: 'Medium', hint: 'Moderate rounding' },
|
|
{ value: 'lg', label: 'Large', hint: 'Very rounded' },
|
|
],
|
|
}),
|
|
variant: () => p.select({
|
|
message: 'UI variant',
|
|
initialValue: 'default',
|
|
options: [
|
|
{ value: 'default', label: 'Default', hint: 'Standard modern look' },
|
|
{ value: 'flat', label: 'Flat', hint: 'Minimal shadows' },
|
|
{ value: 'brutal', label: 'Brutal', hint: 'Bold neo-brutalist style' },
|
|
],
|
|
}),
|
|
density: () => p.select({
|
|
message: 'Spacing density',
|
|
initialValue: 'default',
|
|
options: [
|
|
{ value: 'compact', label: 'Compact', hint: 'Tighter spacing' },
|
|
{ value: 'default', label: 'Default', hint: 'Balanced spacing' },
|
|
{ value: 'comfortable', label: 'Comfortable', hint: 'More breathing room' },
|
|
],
|
|
}),
|
|
}, { onCancel: handleCancel });
|
|
|
|
config.theme = theme;
|
|
|
|
// === AUTHENTICATION ===
|
|
p.log.step(color.cyan('Authentication'));
|
|
|
|
const authProviders = await p.multiselect({
|
|
message: 'Select authentication providers',
|
|
options: [
|
|
{ value: 'credentials', label: 'Email/Password', hint: 'Traditional auth (recommended)' },
|
|
{ value: 'github', label: 'GitHub OAuth', hint: 'Most popular for developers' },
|
|
{ value: 'google', label: 'Google OAuth', hint: 'Widely used' },
|
|
{ value: 'apple', label: 'Apple Sign In', hint: 'Sign in with Apple' },
|
|
{ value: 'azure', label: 'Microsoft Azure AD', hint: 'Enterprise SSO' },
|
|
],
|
|
initialValues: ['credentials'],
|
|
required: false,
|
|
});
|
|
|
|
if (p.isCancel(authProviders)) handleCancel();
|
|
|
|
config.auth.providers = authProviders.length > 0 ? authProviders : ['credentials'];
|
|
|
|
if (config.auth.providers.length === 0) {
|
|
p.log.warn('No providers selected, defaulting to credentials');
|
|
config.auth.providers = ['credentials'];
|
|
}
|
|
|
|
if (config.auth.providers.includes('credentials')) {
|
|
const allowReg = await p.confirm({
|
|
message: 'Allow public registration?',
|
|
initialValue: true,
|
|
});
|
|
if (p.isCancel(allowReg)) handleCancel();
|
|
config.auth.allowRegistration = allowReg;
|
|
} else {
|
|
config.auth.allowRegistration = false;
|
|
}
|
|
|
|
// === INTERNATIONALIZATION ===
|
|
p.log.step(color.cyan('Internationalization'));
|
|
|
|
const selectedLocales = await p.multiselect({
|
|
message: 'Select supported languages',
|
|
options: [
|
|
{ value: 'en', label: 'English', hint: 'Default' },
|
|
{ value: 'es', label: 'Spanish' },
|
|
{ value: 'fr', label: 'French' },
|
|
{ value: 'de', label: 'German' },
|
|
{ value: 'it', label: 'Italian' },
|
|
{ value: 'pt', label: 'Portuguese' },
|
|
{ value: 'tr', label: 'Turkish' },
|
|
{ value: 'az', label: 'Azerbaijani' },
|
|
{ value: 'ja', label: 'Japanese' },
|
|
{ value: 'ko', label: 'Korean' },
|
|
{ value: 'zh', label: 'Chinese' },
|
|
{ value: 'ru', label: 'Russian' },
|
|
{ value: 'ar', label: 'Arabic', hint: 'RTL' },
|
|
{ value: 'fa', label: 'Persian', hint: 'RTL' },
|
|
{ value: 'he', label: 'Hebrew', hint: 'RTL' },
|
|
{ value: 'el', label: 'Greek' },
|
|
],
|
|
required: false,
|
|
});
|
|
|
|
if (p.isCancel(selectedLocales)) handleCancel();
|
|
|
|
config.i18n.locales = selectedLocales.length > 0 ? selectedLocales : ['en'];
|
|
|
|
if (config.i18n.locales.length > 1) {
|
|
const defaultLocale = await p.select({
|
|
message: 'Default locale',
|
|
options: config.i18n.locales.map(l => ({ value: l, label: l })),
|
|
});
|
|
if (p.isCancel(defaultLocale)) handleCancel();
|
|
config.i18n.defaultLocale = defaultLocale;
|
|
} else {
|
|
config.i18n.defaultLocale = config.i18n.locales[0];
|
|
}
|
|
|
|
// === FEATURES ===
|
|
p.log.step(color.cyan('Features'));
|
|
|
|
const features = await p.multiselect({
|
|
message: 'Enable features',
|
|
options: [
|
|
{ value: 'privatePrompts', label: 'Private Prompts', hint: 'Users can create private prompts' },
|
|
{ value: 'changeRequests', label: 'Change Requests', hint: 'Version control system' },
|
|
{ value: 'categories', label: 'Categories', hint: 'Organize prompts by category' },
|
|
{ value: 'tags', label: 'Tags', hint: 'Tag-based organization' },
|
|
{ value: 'comments', label: 'Comments', hint: 'Allow comments on prompts' },
|
|
{ value: 'aiSearch', label: 'AI Search', hint: 'Requires OPENAI_API_KEY' },
|
|
{ value: 'aiGeneration', label: 'AI Generation', hint: 'AI-powered prompt generation (requires OPENAI_API_KEY)' },
|
|
{ value: 'mcp', label: 'MCP Support', hint: 'Model Context Protocol features & API keys' },
|
|
],
|
|
initialValues: ['privatePrompts', 'changeRequests', 'categories', 'tags', 'comments'],
|
|
required: false,
|
|
});
|
|
|
|
if (p.isCancel(features)) handleCancel();
|
|
|
|
config.features = {
|
|
privatePrompts: features.includes('privatePrompts'),
|
|
changeRequests: features.includes('changeRequests'),
|
|
categories: features.includes('categories'),
|
|
tags: features.includes('tags'),
|
|
comments: features.includes('comments'),
|
|
aiSearch: features.includes('aiSearch'),
|
|
aiGeneration: features.includes('aiGeneration'),
|
|
mcp: features.includes('mcp'),
|
|
};
|
|
|
|
// === SPONSORS ===
|
|
p.log.step(color.cyan('Sponsors (Optional)'));
|
|
|
|
const addSponsors = await p.confirm({
|
|
message: 'Add sponsor logos to homepage?',
|
|
initialValue: false,
|
|
});
|
|
|
|
if (p.isCancel(addSponsors)) handleCancel();
|
|
|
|
if (addSponsors) {
|
|
let addMore = true;
|
|
while (addMore) {
|
|
const sponsor = await p.group({
|
|
name: () => p.text({ message: 'Sponsor name', placeholder: 'Acme Inc' }),
|
|
logo: () => p.text({ message: 'Logo URL', placeholder: '/sponsors/acme.svg' }),
|
|
url: () => p.text({ message: 'Website URL', placeholder: 'https://acme.com' }),
|
|
}, { onCancel: handleCancel });
|
|
|
|
if (sponsor.name && sponsor.logo && sponsor.url) {
|
|
config.sponsors.push(sponsor);
|
|
p.log.success(`Added ${sponsor.name}`);
|
|
}
|
|
|
|
const another = await p.confirm({
|
|
message: 'Add another sponsor?',
|
|
initialValue: false,
|
|
});
|
|
if (p.isCancel(another)) handleCancel();
|
|
addMore = another;
|
|
}
|
|
}
|
|
|
|
// === SUMMARY ===
|
|
p.log.step(color.cyan('Configuration Summary'));
|
|
|
|
const summaryLines = [
|
|
`${color.dim('Name:')} ${config.branding.name}`,
|
|
`${color.dim('Description:')} ${config.branding.description}`,
|
|
`${color.dim('Primary Color:')} ${config.theme.primaryColor}`,
|
|
`${color.dim('UI Style:')} ${config.theme.variant} / ${config.theme.radius} radius`,
|
|
`${color.dim('Auth:')} ${config.auth.providers.join(', ')}`,
|
|
`${color.dim('Languages:')} ${config.i18n.locales.join(', ')}`,
|
|
`${color.dim('Features:')} ${Object.entries(config.features).filter(([,v]) => v).map(([k]) => k).join(', ')}`,
|
|
];
|
|
|
|
if (config.sponsors.length > 0) {
|
|
summaryLines.push(`${color.dim('Sponsors:')} ${config.sponsors.map(s => s.name).join(', ')}`);
|
|
}
|
|
|
|
p.note(summaryLines.join('\n'), 'Review your configuration');
|
|
|
|
const proceed = await p.confirm({
|
|
message: 'Generate configuration file?',
|
|
initialValue: true,
|
|
});
|
|
|
|
if (p.isCancel(proceed) || !proceed) {
|
|
p.cancel('Setup cancelled.');
|
|
process.exit(0);
|
|
}
|
|
|
|
// === GENERATE CONFIG ===
|
|
const s = p.spinner();
|
|
s.start('Generating configuration...');
|
|
|
|
// Backup existing config if it exists
|
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
const backupPath = CONFIG_FILE + '.backup';
|
|
fs.copyFileSync(CONFIG_FILE, backupPath);
|
|
}
|
|
|
|
const configContent = generateConfig(config);
|
|
fs.writeFileSync(CONFIG_FILE, configContent);
|
|
|
|
s.stop('Generated prompts.config.ts');
|
|
|
|
// === ENV FILE ===
|
|
s.start('Creating .env file...');
|
|
|
|
const envContent = generateEnvFile(config);
|
|
fs.writeFileSync(ENV_FILE, envContent);
|
|
|
|
s.stop('Created .env file');
|
|
|
|
// === DATABASE SETUP ===
|
|
const setupDb = await p.confirm({
|
|
message: 'Create database and run migrations now?',
|
|
initialValue: true,
|
|
});
|
|
|
|
if (!p.isCancel(setupDb) && setupDb) {
|
|
// Create database
|
|
s.start(`Creating database "${config.env.database.database}"...`);
|
|
try {
|
|
const createDbCmd = `createdb -h ${config.env.database.host} -p ${config.env.database.port} -U ${config.env.database.username} ${config.env.database.database}`;
|
|
execSync(createDbCmd, {
|
|
stdio: 'pipe',
|
|
env: { ...process.env, PGPASSWORD: config.env.database.password }
|
|
});
|
|
s.stop(`Created database "${config.env.database.database}"`);
|
|
} catch (err) {
|
|
if (err.message.includes('already exists')) {
|
|
s.stop(`Database "${config.env.database.database}" already exists`);
|
|
} else {
|
|
s.stop(color.yellow(`Could not create database (may already exist or createdb not available)`));
|
|
p.log.warn(`You may need to create the database manually: createdb ${config.env.database.database}`);
|
|
}
|
|
}
|
|
|
|
// Run db:setup (generate + migrate + seed)
|
|
s.start('Running database setup (prisma generate + migrate)...');
|
|
try {
|
|
execSync('npm run db:setup', {
|
|
stdio: 'inherit',
|
|
cwd: path.join(__dirname, '..'),
|
|
env: { ...process.env, DATABASE_URL: config.env.databaseUrl }
|
|
});
|
|
s.stop('Database setup complete');
|
|
} catch (err) {
|
|
s.stop(color.red('Database setup failed'));
|
|
p.log.error('Run manually: npm run db:setup');
|
|
}
|
|
}
|
|
|
|
// === ADDITIONAL ENV VARS NEEDED ===
|
|
const additionalEnvVars = [];
|
|
|
|
if (config.auth.providers.includes('github')) {
|
|
additionalEnvVars.push('AUTH_GITHUB_ID - GitHub OAuth client ID');
|
|
additionalEnvVars.push('AUTH_GITHUB_SECRET - GitHub OAuth client secret');
|
|
}
|
|
if (config.auth.providers.includes('google')) {
|
|
additionalEnvVars.push('AUTH_GOOGLE_ID - Google OAuth client ID');
|
|
additionalEnvVars.push('AUTH_GOOGLE_SECRET - Google OAuth client secret');
|
|
}
|
|
if (config.auth.providers.includes('azure')) {
|
|
additionalEnvVars.push('AUTH_AZURE_AD_CLIENT_ID - Azure AD client ID');
|
|
additionalEnvVars.push('AUTH_AZURE_AD_CLIENT_SECRET - Azure AD client secret');
|
|
additionalEnvVars.push('AUTH_AZURE_AD_ISSUER - Azure AD issuer URL');
|
|
}
|
|
if (config.auth.providers.includes('apple')) {
|
|
additionalEnvVars.push('AUTH_APPLE_ID - Apple Services ID');
|
|
additionalEnvVars.push('AUTH_APPLE_SECRET - Apple secret key');
|
|
}
|
|
if (config.features.aiSearch || config.features.aiGeneration) {
|
|
additionalEnvVars.push('OPENAI_API_KEY - OpenAI API key for AI features');
|
|
}
|
|
|
|
if (additionalEnvVars.length > 0) {
|
|
p.log.warn(color.yellow('⚠️ Action required: You must configure the following in .env before the app will work:'));
|
|
additionalEnvVars.forEach(v => p.log.message(` ${color.yellow('→')} ${v}`));
|
|
p.log.message('');
|
|
}
|
|
|
|
// === NEXT STEPS ===
|
|
const nextSteps = additionalEnvVars.length > 0
|
|
? `1. Edit ${color.cyan('.env')} to add OAuth credentials (if using OAuth providers)\n` +
|
|
`2. Add your logo files to the ${color.cyan('public/')} folder\n` +
|
|
`3. Run: ${color.cyan('npm run db:push')}\n` +
|
|
`4. Run: ${color.cyan('npm run db:seed')} (optional - seed with prompts)\n` +
|
|
`5. Run: ${color.cyan('npm run dev')}`
|
|
: `1. Add your logo files to the ${color.cyan('public/')} folder\n` +
|
|
`2. Run: ${color.cyan('npm run db:push')}\n` +
|
|
`3. Run: ${color.cyan('npm run db:seed')} (optional - seed with prompts)\n` +
|
|
`4. Run: ${color.cyan('npm run dev')}`;
|
|
|
|
p.note(nextSteps, 'Next steps');
|
|
|
|
p.outro(color.green('Setup complete! See SELF-HOSTING.md for more details.'));
|
|
}
|
|
|
|
main().catch((err) => {
|
|
p.log.error('Setup failed: ' + err.message);
|
|
process.exit(1);
|
|
});
|