#!/usr/bin/env node 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 { try { console.log(chalk.cyan("Starting Docker stack (App, Directus, DB)...")); // Ensure network exists } catch (_e) { // Network already exists or docker is not running } } console.log( chalk.yellow(` 📱 App: http://localhost:3000 🚦 Traefik: http://localhost:8080 `), ); execSync( "docker compose down --remove-orphans && docker compose up -d app", { 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", "pagespeed:test": "mintel pagespeed", }, dependencies: { next: "16.1.6", react: "^19.0.0", "react-dom": "^19.0.0", "@mintel/next-utils": "workspace:*", "@mintel/next-observability": "workspace:*", }, 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 { Sentry } from '@mintel/next-observability'; export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./sentry.server.config'); } if (process.env.NEXT_RUNTIME === 'edge') { await import('./sentry.edge.config'); } } 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 .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\` # Next.js NEXT_PUBLIC_BASE_URL=http://${projectName}.localhost # Sentry / Glitchtip SENTRY_DSN= # Analytics (Umami) UMAMI_WEBSITE_ID= UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me # Notifications (Gotify) GOTIFY_URL= GOTIFY_TOKEN= `; 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();