feat(scripts): add interactive setup script for private clone configuration
This commit is contained in:
@@ -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)**
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user