Files
awesome-chatgpt-prompts-pro…/scripts/setup.js
2026-01-06 10:55:04 +03:00

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);
});