import { Command } from "commander"; import fs from "fs-extra"; import path from "path"; import chalk from "chalk"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const program = new Command(); program .name("mintel") .description("CLI for Mintel monorepo management") .version("1.0.1"); program .command("dev") .description("Start the development environment (Docker stack)") .option("-l, --local", "Run Next.js locally instead of in Docker") .action(async (options) => { const { execSync } = await import("child_process"); console.log(chalk.blue("🚀 Starting Development Environment...")); if (options.local) { console.log(chalk.cyan("Running Next.js locally...")); execSync("next dev", { stdio: "inherit" }); } else { console.log(chalk.cyan("Starting Docker stack (App, Directus, DB)...")); // Ensure network exists try { execSync("docker network create infra", { stdio: "ignore" }); } catch (e) {} console.log( chalk.yellow(` 📱 App: http://localhost:3000 🗄️ CMS: http://localhost:8055/admin 🚦 Traefik: http://localhost:8080 `), ); execSync( "docker-compose down --remove-orphans && docker-compose up app directus directus-db", { stdio: "inherit" }, ); } }); const directus = program .command("directus") .description("Directus management commands"); directus .command("bootstrap") .description("Setup Directus branding and settings") .action(async () => { const { execSync } = await import("child_process"); console.log(chalk.blue("🎨 Bootstrapping Directus...")); execSync("npx tsx --env-file=.env scripts/setup-directus.ts", { stdio: "inherit", }); }); directus .command("sync ") .description("Sync Directus data (push/pull) for a specific environment") .action(async (action, env) => { const { execSync } = await import("child_process"); console.log( chalk.blue(`📥 Executing Directus sync: ${action} -> ${env}...`), ); execSync(`./scripts/sync-directus.sh ${action} ${env}`, { stdio: "inherit", }); }); program .command("pagespeed") .description("Run PageSpeed (Lighthouse) tests") .action(async () => { const { execSync } = await import("child_process"); console.log(chalk.blue("⚡ Running PageSpeed tests...")); execSync("npx tsx ./scripts/pagespeed-sitemap.ts", { stdio: "inherit" }); }); program .command("init ") .description("Initialize a new website project") .action(async (projectPath) => { const fullPath = path.isAbsolute(projectPath) ? projectPath : path.resolve(process.cwd(), "../../", projectPath); const projectName = path.basename(fullPath); console.log(chalk.blue(`Initializing new project: ${projectName}...`)); try { // Create directory await fs.ensureDir(fullPath); // Create package.json const pkgJson = { name: projectName, version: "0.1.0", private: true, type: "module", scripts: { dev: "mintel dev", "dev:local": "mintel dev --local", build: "next build", start: "next start", lint: "next lint", typecheck: "tsc --noEmit", test: "vitest run --passWithNoTests", "directus:bootstrap": "mintel directus bootstrap", "directus:push:testing": "mintel directus sync push testing", "directus:pull:testing": "mintel directus sync pull testing", "directus:push:staging": "mintel directus sync push staging", "directus:pull:staging": "mintel directus sync pull staging", "directus:push:prod": "mintel directus sync push production", "directus:pull:prod": "mintel directus sync pull production", "pagespeed:test": "mintel pagespeed", }, dependencies: { next: "15.1.6", react: "^19.0.0", "react-dom": "^19.0.0", "@mintel/next-utils": "workspace:*", "@directus/sdk": "^21.0.0", }, devDependencies: { "@types/node": "^20.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", typescript: "^5.0.0", "@mintel/tsconfig": "workspace:*", "@mintel/eslint-config": "workspace:*", "@mintel/next-config": "workspace:*", "@mintel/husky-config": "workspace:*", }, }; await fs.writeJson(path.join(fullPath, "package.json"), pkgJson, { spaces: 2, }); // Create next.config.ts const nextConfig = `import mintelNextConfig from "@mintel/next-config"; /** @type {import('next').NextConfig} */ const nextConfig = {}; export default mintelNextConfig(nextConfig); `; await fs.writeFile(path.join(fullPath, "next.config.ts"), nextConfig); // Create tsconfig.json const tsConfig = { extends: "@mintel/tsconfig/nextjs.json", compilerOptions: { paths: { "@/*": ["./src/*"], }, }, include: [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ], exclude: ["node_modules"], }; await fs.writeJson(path.join(fullPath, "tsconfig.json"), tsConfig, { spaces: 2, }); // Create eslint.config.mjs const eslintConfig = `import { nextConfig } from "@mintel/eslint-config/next"; export default nextConfig; `; await fs.writeFile( path.join(fullPath, "eslint.config.mjs"), eslintConfig, ); // Create Husky/Linting configs await fs.writeFile( path.join(fullPath, "commitlint.config.js"), `export { default } from "@mintel/husky-config/commitlint";\n`, ); await fs.writeFile( path.join(fullPath, ".lintstagedrc.js"), `export { default } from "@mintel/husky-config/lint-staged";\n`, ); // Create env validation script await fs.ensureDir(path.join(fullPath, "scripts")); await fs.writeFile( path.join(fullPath, "scripts/validate-env.ts"), `import { validateMintelEnv } from "@mintel/next-utils"; try { validateMintelEnv(); console.log("✅ Environment variables validated"); } catch (error) { process.exit(1); } `, ); // Create basic src structure await fs.ensureDir(path.join(fullPath, "src/app/[locale]")); await fs.writeFile( path.join(fullPath, "src/middleware.ts"), `import { createMintelMiddleware } from "@mintel/next-utils"; export default createMintelMiddleware({ locales: ["en", "de"], defaultLocale: "en", logRequests: true, }); export const config = { matcher: ["/((?!api|_next|_vercel|health|.*\\\\..*).*)", "/", "/(de|en)/:path*"] }; `, ); // Create i18n/request.ts await fs.ensureDir(path.join(fullPath, "src/i18n")); await fs.writeFile( path.join(fullPath, "src/i18n/request.ts"), `import { createMintelI18nRequestConfig } from "@mintel/next-utils"; export default createMintelI18nRequestConfig( ["en", "de"], "en", (locale) => import(\`../../messages/\${locale}.json\`) ); `, ); // Create messages directory await fs.ensureDir(path.join(fullPath, "messages")); await fs.writeJson( path.join(fullPath, "messages/en.json"), { Index: { title: "Welcome", }, }, { spaces: 2 }, ); await fs.writeJson( path.join(fullPath, "messages/de.json"), { Index: { title: "Willkommen", }, }, { spaces: 2 }, ); // Create instrumentation.ts await fs.writeFile( path.join(fullPath, "src/instrumentation.ts"), `import * as Sentry from '@sentry/nextjs'; export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { // Server-side initialization } } export const onRequestError = Sentry.captureRequestError; `, ); await fs.writeFile( path.join(fullPath, "src/app/[locale]/layout.tsx"), `import type { Metadata } from "next"; export const metadata: Metadata = { title: "${projectName}", description: "Created with Mintel CLI", }; export default function RootLayout({ children, params: { locale } }: { children: React.ReactNode; params: { locale: string }; }) { return ( {children} ); } `, ); await fs.writeFile( path.join(fullPath, "src/app/[locale]/page.tsx"), `import { useTranslations } from 'next-intl'; export default function Home() { const t = useTranslations('Index'); return (

{t('title')} to ${projectName}

); } `, ); // Copy infra templates const infraPath = path.resolve(__dirname, "../../infra"); if (await fs.pathExists(infraPath)) { // Setup Dockerfile from template const templatePath = path.join( infraPath, "docker/Dockerfile.app-template", ); if (await fs.pathExists(templatePath)) { let dockerfile = await fs.readFile(templatePath, "utf8"); dockerfile = dockerfile.replace(/\$\{APP_NAME:-app\}/g, projectName); await fs.writeFile(path.join(fullPath, "Dockerfile"), dockerfile); } // Setup docker-compose from template const composeTemplatePath = path.join( infraPath, "docker/docker-compose.template.yml", ); if (await fs.pathExists(composeTemplatePath)) { let compose = await fs.readFile(composeTemplatePath, "utf8"); compose = compose.replace(/\$\{APP_NAME:-app\}/g, projectName); compose = compose.replace(/\$\{PROJECT_NAME:-app\}/g, projectName); await fs.writeFile( path.join(fullPath, "docker-compose.yml"), compose, ); } await fs.ensureDir(path.join(fullPath, ".gitea/workflows")); const deployActionPath = path.join( infraPath, "gitea/deploy-action.yml", ); if (await fs.pathExists(deployActionPath)) { await fs.copy( deployActionPath, path.join(fullPath, ".gitea/workflows/deploy.yml"), ); } } // Create Directus structure await fs.ensureDir(path.join(fullPath, "directus/uploads")); await fs.ensureDir(path.join(fullPath, "directus/extensions")); await fs.writeFile(path.join(fullPath, "directus/uploads/.gitkeep"), ""); await fs.writeFile( path.join(fullPath, "directus/extensions/.gitkeep"), "", ); // Create .env.example const envExample = `# Project PROJECT_NAME=${projectName} PROJECT_COLOR=#82ed20 # Authentication GATEKEEPER_PASSWORD=mintel AUTH_COOKIE_NAME=mintel_gatekeeper_session # Host Config (Local) TRAEFIK_HOST=\`${projectName}.localhost\` DIRECTUS_HOST=\`cms.${projectName}.localhost\` # Next.js NEXT_PUBLIC_BASE_URL=http://${projectName}.localhost # Directus DIRECTUS_URL=http://cms.${projectName}.localhost DIRECTUS_KEY=$(openssl rand -hex 32 2>/dev/null || echo "mintel-key") DIRECTUS_SECRET=$(openssl rand -hex 32 2>/dev/null || echo "mintel-secret") DIRECTUS_ADMIN_EMAIL=admin@mintel.me DIRECTUS_ADMIN_PASSWORD=mintel-admin-pass DIRECTUS_DB_NAME=directus DIRECTUS_DB_USER=directus DIRECTUS_DB_PASSWORD=mintel-db-pass # Sentry / Glitchtip SENTRY_DSN= # Analytics (Umami) NEXT_PUBLIC_UMAMI_WEBSITE_ID= NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js `; await fs.writeFile(path.join(fullPath, ".env.example"), envExample); // Copy premium templates (globals.css, lib/directus.ts, scripts/setup-directus.ts) const templatePath = path.join(infraPath, "templates/website"); if (await fs.pathExists(templatePath)) { console.log(chalk.blue("Applying premium templates...")); await fs.copy(templatePath, fullPath, { overwrite: true }); } console.log( chalk.green(`Successfully initialized ${projectName} at ${fullPath}`), ); console.log(chalk.yellow("\nNext steps:")); console.log(chalk.cyan("1. pnpm install")); console.log(chalk.cyan(`2. cd ${projectPath} && pnpm dev`)); } catch (error) { console.error(chalk.red("Error initializing project:"), error); } }); program.parse();