feat: comprehensive boilerplate improvements
Security & Stability: - Add Next.js 16 proxy.ts for BetterAuth cookie-based auth protection - Add rate limiting for API routes (src/lib/rate-limit.ts) - Add Zod validation for chat API request bodies - Add session auth check to chat and diagnostics endpoints - Add security headers in next.config.ts (CSP, X-Frame-Options, etc.) - Add file upload validation and sanitization in storage.ts Core UX Components: - Add error boundaries (error.tsx, not-found.tsx, chat/error.tsx) - Add loading states (skeleton.tsx, spinner.tsx, loading.tsx files) - Add toast notifications with Sonner - Add form components (input.tsx, textarea.tsx, label.tsx) - Add database indexes for performance (schema.ts) - Enhance chat UX: timestamps, copy-to-clipboard, thinking indicator, error display, localStorage message persistence Polish & Accessibility: - Add Open Graph and Twitter card metadata - Add JSON-LD structured data for SEO - Add sitemap.ts, robots.ts, manifest.ts - Add skip-to-content link and ARIA labels in site-header - Enable profile page quick action buttons with dialogs - Update Next.js 15 references to Next.js 16 Developer Experience: - Add GitHub Actions CI workflow (lint, typecheck, build) - Add Prettier configuration (.prettierrc, .prettierignore) - Add .nvmrc pinning Node 20 - Add ESLint rules: import/order, react-hooks/exhaustive-deps - Add stricter TypeScript settings (exactOptionalPropertyTypes, noImplicitOverride) - Add interactive setup script (scripts/setup.ts) - Add session utility functions (src/lib/session.ts) All changes mirrored to create-agentic-app/template/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
274
scripts/setup.ts
Normal file
274
scripts/setup.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Interactive setup wizard for the Agentic Coding Starter Kit.
|
||||
* Run with: npx tsx scripts/setup.ts
|
||||
*/
|
||||
|
||||
import { existsSync, copyFileSync, readFileSync } from "fs";
|
||||
import { createInterface } from "readline";
|
||||
import { execSync } from "child_process";
|
||||
import { join } from "path";
|
||||
|
||||
const ROOT_DIR = join(import.meta.dirname, "..");
|
||||
const ENV_EXAMPLE = join(ROOT_DIR, "env.example");
|
||||
const ENV_FILE = join(ROOT_DIR, ".env");
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
red: "\x1b[31m",
|
||||
cyan: "\x1b[36m",
|
||||
dim: "\x1b[2m",
|
||||
};
|
||||
|
||||
function log(message: string, color?: keyof typeof colors) {
|
||||
const colorCode = color ? colors[color] : "";
|
||||
console.log(`${colorCode}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function header(message: string) {
|
||||
console.log();
|
||||
log(`${"=".repeat(60)}`, "cyan");
|
||||
log(` ${message}`, "bright");
|
||||
log(`${"=".repeat(60)}`, "cyan");
|
||||
console.log();
|
||||
}
|
||||
|
||||
function success(message: string) {
|
||||
log(`✓ ${message}`, "green");
|
||||
}
|
||||
|
||||
function warn(message: string) {
|
||||
log(`⚠ ${message}`, "yellow");
|
||||
}
|
||||
|
||||
function error(message: string) {
|
||||
log(`✗ ${message}`, "red");
|
||||
}
|
||||
|
||||
function info(message: string) {
|
||||
log(` ${message}`, "dim");
|
||||
}
|
||||
|
||||
async function prompt(question: string): Promise<string> {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${colors.cyan}? ${colors.reset}${question} `, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function confirm(question: string): Promise<boolean> {
|
||||
const answer = await prompt(`${question} (y/n)`);
|
||||
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
||||
}
|
||||
|
||||
function checkNodeVersion(): boolean {
|
||||
const requiredMajor = 20;
|
||||
const currentVersion = process.version;
|
||||
const currentMajor = parseInt(currentVersion.slice(1).split(".")[0] || "0", 10);
|
||||
|
||||
if (currentMajor >= requiredMajor) {
|
||||
success(`Node.js ${currentVersion} detected (requires v${requiredMajor}+)`);
|
||||
return true;
|
||||
} else {
|
||||
error(`Node.js ${currentVersion} detected, but v${requiredMajor}+ is required`);
|
||||
info("Please upgrade Node.js: https://nodejs.org/");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function copyEnvFile(): boolean {
|
||||
if (existsSync(ENV_FILE)) {
|
||||
warn(".env file already exists");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!existsSync(ENV_EXAMPLE)) {
|
||||
error("env.example file not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
copyFileSync(ENV_EXAMPLE, ENV_FILE);
|
||||
success("Created .env file from env.example");
|
||||
return true;
|
||||
} catch (err) {
|
||||
error(`Failed to create .env file: ${err}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface EnvStatus {
|
||||
configured: string[];
|
||||
missing: string[];
|
||||
optional: string[];
|
||||
}
|
||||
|
||||
function checkEnvVariables(): EnvStatus {
|
||||
const required = ["POSTGRES_URL", "BETTER_AUTH_SECRET"];
|
||||
const optional = [
|
||||
"GOOGLE_CLIENT_ID",
|
||||
"GOOGLE_CLIENT_SECRET",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENROUTER_MODEL",
|
||||
"BLOB_READ_WRITE_TOKEN",
|
||||
"NEXT_PUBLIC_APP_URL",
|
||||
];
|
||||
|
||||
const status: EnvStatus = {
|
||||
configured: [],
|
||||
missing: [],
|
||||
optional: [],
|
||||
};
|
||||
|
||||
// Read .env file if it exists
|
||||
let envContent = "";
|
||||
if (existsSync(ENV_FILE)) {
|
||||
envContent = readFileSync(ENV_FILE, "utf-8");
|
||||
}
|
||||
|
||||
// Parse env file (simple key=value parsing)
|
||||
const envVars: Record<string, string> = {};
|
||||
envContent.split("\n").forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith("#")) {
|
||||
const [key, ...valueParts] = trimmed.split("=");
|
||||
if (key) {
|
||||
envVars[key] = valueParts.join("=");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check required variables
|
||||
for (const key of required) {
|
||||
const value = envVars[key];
|
||||
if (value && value.length > 0 && !value.startsWith("your-")) {
|
||||
status.configured.push(key);
|
||||
} else {
|
||||
status.missing.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Check optional variables
|
||||
for (const key of optional) {
|
||||
const value = envVars[key];
|
||||
if (value && value.length > 0 && !value.startsWith("your-")) {
|
||||
status.configured.push(key);
|
||||
} else {
|
||||
status.optional.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
async function runDatabaseMigration(): Promise<boolean> {
|
||||
log("\nRunning database migration...", "cyan");
|
||||
|
||||
try {
|
||||
execSync("pnpm db:migrate", {
|
||||
cwd: ROOT_DIR,
|
||||
stdio: "inherit",
|
||||
});
|
||||
success("Database migration completed");
|
||||
return true;
|
||||
} catch {
|
||||
error("Database migration failed");
|
||||
info("Make sure your database is running and POSTGRES_URL is correct");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function printNextSteps(envStatus: EnvStatus) {
|
||||
header("Next Steps");
|
||||
|
||||
const steps: string[] = [];
|
||||
|
||||
if (envStatus.missing.length > 0) {
|
||||
steps.push(`Configure required env vars in .env: ${envStatus.missing.join(", ")}`);
|
||||
}
|
||||
|
||||
if (envStatus.optional.includes("GOOGLE_CLIENT_ID")) {
|
||||
steps.push("Set up Google OAuth at https://console.cloud.google.com/");
|
||||
}
|
||||
|
||||
if (envStatus.optional.includes("OPENROUTER_API_KEY")) {
|
||||
steps.push("Get an OpenRouter API key at https://openrouter.ai/settings/keys");
|
||||
}
|
||||
|
||||
steps.push("Start the development server: pnpm dev");
|
||||
steps.push("Open http://localhost:3000 in your browser");
|
||||
|
||||
steps.forEach((step, index) => {
|
||||
log(`${index + 1}. ${step}`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
log("Documentation:", "bright");
|
||||
info("- README.md - Project overview and setup");
|
||||
info("- CLAUDE.md - AI assistant guidelines");
|
||||
info("- docs/ - Technical documentation");
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
header("Agentic Coding Starter Kit - Setup Wizard");
|
||||
|
||||
// Step 1: Check Node version
|
||||
log("Checking Node.js version...", "cyan");
|
||||
if (!checkNodeVersion()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 2: Create .env file
|
||||
console.log();
|
||||
log("Setting up environment...", "cyan");
|
||||
copyEnvFile();
|
||||
|
||||
// Step 3: Check environment variables
|
||||
console.log();
|
||||
log("Checking environment variables...", "cyan");
|
||||
const envStatus = checkEnvVariables();
|
||||
|
||||
if (envStatus.configured.length > 0) {
|
||||
success(`Configured: ${envStatus.configured.join(", ")}`);
|
||||
}
|
||||
if (envStatus.missing.length > 0) {
|
||||
warn(`Missing (required): ${envStatus.missing.join(", ")}`);
|
||||
}
|
||||
if (envStatus.optional.length > 0) {
|
||||
info(`Optional (not set): ${envStatus.optional.join(", ")}`);
|
||||
}
|
||||
|
||||
// Step 4: Offer to run database migration
|
||||
if (envStatus.missing.length === 0) {
|
||||
console.log();
|
||||
const shouldMigrate = await confirm("Would you like to run database migrations now?");
|
||||
if (shouldMigrate) {
|
||||
await runDatabaseMigration();
|
||||
}
|
||||
} else {
|
||||
console.log();
|
||||
warn("Skipping database migration - please configure required env vars first");
|
||||
}
|
||||
|
||||
// Step 5: Print next steps
|
||||
printNextSteps(envStatus);
|
||||
|
||||
log("Setup complete!", "green");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
error(`Setup failed: ${err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user