From fdad60ba98688335d8b289838e912d4bf187249e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20Kadir=20Ak=C4=B1n?= Date: Sat, 13 Dec 2025 19:50:14 +0300 Subject: [PATCH] feat(scripts): add interactive setup script for private clone configuration --- README.md | 34 +++++ package.json | 1 + scripts/setup.js | 385 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 420 insertions(+) create mode 100644 scripts/setup.js diff --git a/README.md b/README.md index 903841b7..79fdffcb 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,40 @@ npm run db:push npm run dev ``` +### Private Clone Setup (Recommended for Teams) + +For private deployments, use our **interactive setup wizard** to configure your instance with custom branding, disable sponsorship sections, and set up authentication: + +```bash +git clone https://github.com/f/awesome-chatgpt-prompts.git +cd awesome-chatgpt-prompts +npm install +npm run setup +``` + +The setup wizard will guide you through: +- **Branding** — Set your organization name, logo, and description +- **Theme** — Choose colors, border radius, and UI style +- **Authentication** — Configure GitHub, Google, Azure AD, or email/password login +- **Features** — Enable/disable private prompts, categories, tags, AI search +- **Languages** — Select supported locales +- **Sponsors** — Optionally add your own sponsor logos (prompts.chat sponsors are disabled) + +After setup, complete the configuration: + +```bash +# Edit .env with your database and OAuth credentials +nano .env + +# Run database migrations +npm run db:push + +# Start development server +npm run dev +``` + +> 💡 **Tip:** The setup script automatically enables "clone branding mode" which hides prompts.chat branding, achievements, and sponsors from the homepage. + We hope you find these prompts useful and have fun exploring AI chat models! **[View on prompts.chat](https://prompts.chat)** diff --git a/package.json b/package.json index 1796ff02..8afffedd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "db:push": "prisma db push", "db:studio": "prisma studio", "db:seed": "prisma db seed", + "setup": "node scripts/setup.js", "postinstall": "prisma generate" }, "dependencies": { diff --git a/scripts/setup.js b/scripts/setup.js new file mode 100644 index 00000000..de1a274d --- /dev/null +++ b/scripts/setup.js @@ -0,0 +1,385 @@ +#!/usr/bin/env node + +/** + * 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 readline = require('readline'); + +const CONFIG_FILE = path.join(__dirname, '..', 'prompts.config.ts'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +function question(prompt, defaultValue = '') { + const defaultText = defaultValue ? ` (${defaultValue})` : ''; + return new Promise((resolve) => { + rl.question(`${prompt}${defaultText}: `, (answer) => { + resolve(answer.trim() || defaultValue); + }); + }); +} + +function confirm(prompt, defaultValue = true) { + const hint = defaultValue ? '[Y/n]' : '[y/N]'; + return new Promise((resolve) => { + rl.question(`${prompt} ${hint}: `, (answer) => { + if (!answer.trim()) { + resolve(defaultValue); + } else { + resolve(answer.toLowerCase().startsWith('y')); + } + }); + }); +} + +function select(prompt, options, defaultIndex = 0) { + console.log(`\n${prompt}`); + options.forEach((opt, i) => { + const marker = i === defaultIndex ? '>' : ' '; + console.log(` ${marker} ${i + 1}. ${opt.label}`); + }); + return new Promise((resolve) => { + rl.question(`Enter choice (1-${options.length}) [${defaultIndex + 1}]: `, (answer) => { + const index = answer.trim() ? parseInt(answer) - 1 : defaultIndex; + if (index >= 0 && index < options.length) { + resolve(options[index].value); + } else { + resolve(options[defaultIndex].value); + } + }); + }); +} + +function multiSelect(prompt, options) { + console.log(`\n${prompt}`); + options.forEach((opt, i) => { + console.log(` ${i + 1}. ${opt.label}`); + }); + return new Promise((resolve) => { + rl.question(`Enter choices (comma-separated, e.g., 1,2): `, (answer) => { + if (!answer.trim()) { + resolve([]); + return; + } + const indices = answer.split(',').map(s => parseInt(s.trim()) - 1); + const selected = indices + .filter(i => i >= 0 && i < options.length) + .map(i => options[i].value); + resolve(selected); + }); + }); +} + +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(p => `"${p}"`).join(', ')}], + allowRegistration: ${config.auth.allowRegistration}, + }, + + // Storage plugin for media uploads + storage: { + provider: "${config.storage.provider}", + }, + + // 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}, + aiSearch: ${config.features.aiSearch}, + }, + + // Homepage customization (clone branding mode) + homepage: { + useCloneBranding, + achievements: { + enabled: false, // Disabled for private clones + }, + sponsors: { + enabled: ${config.sponsors.length > 0}, + items: ${sponsorsSection}, + }, + }, +}); +`; +} + +async function main() { + console.log('\n╔═══════════════════════════════════════════════════════════╗'); + console.log('║ prompts.chat - Private Clone Setup ║'); + console.log('╠═══════════════════════════════════════════════════════════╣'); + console.log('║ This wizard will help you configure your private ║'); + console.log('║ prompt library with your own branding and settings. ║'); + console.log('╚═══════════════════════════════════════════════════════════╝\n'); + + const config = { + branding: {}, + theme: {}, + auth: {}, + storage: {}, + i18n: {}, + features: {}, + sponsors: [] + }; + + // === BRANDING === + console.log('\n─── BRANDING ───────────────────────────────────────────────\n'); + + config.branding.name = await question('Organization/App name', 'My Prompt Library'); + config.branding.description = await question('Description', 'Collect, organize, and share AI prompts'); + config.branding.logo = await question('Logo path (public folder)', '/logo.svg'); + config.branding.logoDark = await question('Dark mode logo path', config.branding.logo); + config.branding.favicon = await question('Favicon path', '/logo.svg'); + + // === THEME === + console.log('\n─── THEME ──────────────────────────────────────────────────\n'); + + config.theme.primaryColor = await question('Primary color (hex)', '#6366f1'); + + config.theme.radius = await select('Border radius style:', [ + { label: 'None - Sharp corners', value: 'none' }, + { label: 'Small - Subtle rounding', value: 'sm' }, + { label: 'Medium - Moderate rounding', value: 'md' }, + { label: 'Large - Very rounded', value: 'lg' } + ], 1); + + config.theme.variant = await select('UI variant:', [ + { label: 'Default - Standard modern look', value: 'default' }, + { label: 'Flat - Minimal shadows', value: 'flat' }, + { label: 'Brutal - Bold neo-brutalist style', value: 'brutal' } + ], 0); + + config.theme.density = await select('Spacing density:', [ + { label: 'Compact - Tighter spacing', value: 'compact' }, + { label: 'Default - Balanced spacing', value: 'default' }, + { label: 'Comfortable - More breathing room', value: 'comfortable' } + ], 1); + + // === AUTHENTICATION === + console.log('\n─── AUTHENTICATION ─────────────────────────────────────────\n'); + + config.auth.providers = await multiSelect('Select authentication providers:', [ + { label: 'GitHub OAuth', value: 'github' }, + { label: 'Google OAuth', value: 'google' }, + { label: 'Microsoft Azure AD', value: 'azure' }, + { label: 'Email/Password (Credentials)', value: 'credentials' } + ]); + + if (config.auth.providers.length === 0) { + console.log(' ⚠️ No providers selected, defaulting to credentials'); + config.auth.providers = ['credentials']; + } + + if (config.auth.providers.includes('credentials')) { + config.auth.allowRegistration = await confirm('Allow public registration?', true); + } else { + config.auth.allowRegistration = false; + } + + // === STORAGE === + console.log('\n─── STORAGE ────────────────────────────────────────────────\n'); + + config.storage.provider = await select('Media storage provider:', [ + { label: 'URL-based (external links)', value: 'url' }, + { label: 'AWS S3 (requires configuration)', value: 's3' } + ], 0); + + // === INTERNATIONALIZATION === + console.log('\n─── INTERNATIONALIZATION ───────────────────────────────────\n'); + + const selectedLocales = await multiSelect('Select supported languages:', [ + { label: 'English', value: 'en' }, + { label: 'Spanish', value: 'es' }, + { label: 'French', value: 'fr' }, + { label: 'German', value: 'de' }, + { label: 'Italian', value: 'it' }, + { label: 'Portuguese', value: 'pt' }, + { label: 'Turkish', value: 'tr' }, + { label: 'Japanese', value: 'ja' }, + { label: 'Korean', value: 'ko' }, + { label: 'Chinese', value: 'zh' }, + { label: 'Arabic', value: 'ar' } + ]); + + config.i18n.locales = selectedLocales.length > 0 ? selectedLocales : ['en']; + + if (config.i18n.locales.length > 1) { + console.log(`\nSelected locales: ${config.i18n.locales.join(', ')}`); + config.i18n.defaultLocale = await question('Default locale', config.i18n.locales[0]); + } else { + config.i18n.defaultLocale = config.i18n.locales[0]; + } + + // === FEATURES === + console.log('\n─── FEATURES ───────────────────────────────────────────────\n'); + + config.features.privatePrompts = await confirm('Enable private prompts?', true); + config.features.changeRequests = await confirm('Enable change request system (versioning)?', true); + config.features.categories = await confirm('Enable categories?', true); + config.features.tags = await confirm('Enable tags?', true); + config.features.aiSearch = await confirm('Enable AI-powered search? (requires OPENAI_API_KEY)', false); + + // === SPONSORS === + console.log('\n─── SPONSORS (Optional) ────────────────────────────────────\n'); + + const addSponsors = await confirm('Add sponsor logos to homepage?', false); + + if (addSponsors) { + let addMore = true; + while (addMore) { + console.log('\n Adding sponsor:'); + const name = await question(' Sponsor name'); + const logo = await question(' Logo URL'); + const url = await question(' Website URL'); + + if (name && logo && url) { + config.sponsors.push({ name, logo, url }); + console.log(` ✓ Added ${name}`); + } + + addMore = await confirm(' Add another sponsor?', false); + } + } + + // === SUMMARY === + console.log('\n─── CONFIGURATION SUMMARY ──────────────────────────────────\n'); + console.log(` Name: ${config.branding.name}`); + console.log(` Description: ${config.branding.description}`); + console.log(` Primary Color: ${config.theme.primaryColor}`); + console.log(` UI Style: ${config.theme.variant} / ${config.theme.radius} radius`); + console.log(` Auth: ${config.auth.providers.join(', ')}`); + console.log(` Storage: ${config.storage.provider}`); + console.log(` Languages: ${config.i18n.locales.join(', ')}`); + console.log(` Features: ${Object.entries(config.features).filter(([,v]) => v).map(([k]) => k).join(', ')}`); + if (config.sponsors.length > 0) { + console.log(` Sponsors: ${config.sponsors.map(s => s.name).join(', ')}`); + } + console.log(''); + + const proceed = await confirm('Generate configuration file?', true); + + if (!proceed) { + console.log('\n❌ Setup cancelled.\n'); + rl.close(); + process.exit(0); + } + + // === GENERATE CONFIG === + const configContent = generateConfig(config); + + // Backup existing config if it exists + if (fs.existsSync(CONFIG_FILE)) { + const backupPath = CONFIG_FILE + '.backup'; + fs.copyFileSync(CONFIG_FILE, backupPath); + console.log(`\n📦 Backed up existing config to prompts.config.ts.backup`); + } + + fs.writeFileSync(CONFIG_FILE, configContent); + console.log('✅ Generated prompts.config.ts'); + + // === ENV FILE === + console.log('\n─── ENVIRONMENT VARIABLES ──────────────────────────────────\n'); + + const envExample = path.join(__dirname, '..', '.env.example'); + const envFile = path.join(__dirname, '..', '.env'); + + if (!fs.existsSync(envFile) && fs.existsSync(envExample)) { + const createEnv = await confirm('Create .env file from .env.example?', true); + if (createEnv) { + fs.copyFileSync(envExample, envFile); + console.log('✅ Created .env file'); + } + } + + console.log('\n📋 Required environment variables:'); + console.log(' DATABASE_URL - PostgreSQL connection string'); + console.log(' AUTH_SECRET - NextAuth secret (generate with: openssl rand -base64 32)'); + + if (config.auth.providers.includes('github')) { + console.log(' AUTH_GITHUB_ID - GitHub OAuth client ID'); + console.log(' AUTH_GITHUB_SECRET - GitHub OAuth client secret'); + } + if (config.auth.providers.includes('google')) { + console.log(' AUTH_GOOGLE_ID - Google OAuth client ID'); + console.log(' AUTH_GOOGLE_SECRET - Google OAuth client secret'); + } + if (config.auth.providers.includes('azure')) { + console.log(' AUTH_AZURE_AD_CLIENT_ID - Azure AD client ID'); + console.log(' AUTH_AZURE_AD_CLIENT_SECRET - Azure AD client secret'); + console.log(' AUTH_AZURE_AD_ISSUER - Azure AD issuer URL'); + } + if (config.features.aiSearch) { + console.log(' OPENAI_API_KEY - OpenAI API key for semantic search'); + } + if (config.storage.provider === 's3') { + console.log(' AWS_ACCESS_KEY_ID - AWS access key'); + console.log(' AWS_SECRET_ACCESS_KEY - AWS secret key'); + console.log(' AWS_S3_BUCKET - S3 bucket name'); + console.log(' AWS_S3_REGION - S3 region'); + } + + // === NEXT STEPS === + console.log('\n─── NEXT STEPS ─────────────────────────────────────────────\n'); + console.log(' 1. Edit .env with your database and auth credentials'); + console.log(' 2. Add your logo files to the public/ folder'); + console.log(' 3. Run: npm run db:push'); + console.log(' 4. Run: npm run dev'); + console.log(''); + console.log('📖 For more details, see SELF-HOSTING.md'); + console.log('\n═══════════════════════════════════════════════════════════════\n'); + + rl.close(); +} + +main().catch((err) => { + console.error('Setup failed:', err); + rl.close(); + process.exit(1); +});