feat(scripts): add interactive setup script for private clone configuration

This commit is contained in:
Fatih Kadir Akın
2025-12-13 19:50:14 +03:00
parent 08f025ec20
commit fdad60ba98
3 changed files with 420 additions and 0 deletions
+34
View File
@@ -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)**
+1
View File
@@ -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": {
+385
View File
@@ -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);
});