#!/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 🗄️ 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("bootstrap-feedback") .description("Setup Directus collections and flows for Feedback") .action(async () => { const { execSync } = await import("child_process"); console.log(chalk.blue("📧 Bootstrapping Visual Feedback System...")); // Use the logic from setup-feedback-hardened.ts const bootstrapScript = ` import { createDirectus, rest, authentication, createCollection, createDashboard, createPanel, createItems, createPermission, readPolicies, readRoles, readUsers } from '@directus/sdk'; async function setup() { const url = process.env.DIRECTUS_URL || 'http://localhost:8055'; const email = process.env.DIRECTUS_ADMIN_EMAIL; const password = process.env.DIRECTUS_ADMIN_PASSWORD; if (!email || !password) { console.error('❌ DIRECTUS_ADMIN_EMAIL or DIRECTUS_ADMIN_PASSWORD not set'); process.exit(1); } const client = createDirectus(url).with(authentication('json')).with(rest()); try { console.log('🔑 Authenticating...'); await client.login(email, password); const roles = await client.request(readRoles()); const adminRole = roles.find(r => r.name === 'Administrator'); const policies = await client.request(readPolicies()); const adminPolicy = policies.find(p => p.name === 'Administrator'); console.log('🏗️ Creating Collection "visual_feedback"...'); try { await client.request(createCollection({ collection: 'visual_feedback', meta: { icon: 'feedback', display_template: '{{user_name}}: {{text}}' }, fields: [ { field: 'id', type: 'uuid', schema: { is_primary_key: true } }, { field: 'status', type: 'string', schema: { default_value: 'open' }, meta: { interface: 'select-dropdown' } }, { field: 'url', type: 'string' }, { field: 'selector', type: 'string' }, { field: 'x', type: 'float' }, { field: 'y', type: 'float' }, { field: 'type', type: 'string' }, { field: 'text', type: 'text' }, { field: 'user_name', type: 'string' }, { field: 'user_identity', type: 'string' }, { field: 'screenshot', type: 'uuid', meta: { interface: 'file' } }, { field: 'date_created', type: 'timestamp', schema: { default_value: 'NOW()' } } ] } as any)); } catch (_e) { console.log(' (Collection might already exist)'); } try { await client.request(createCollection({ collection: 'visual_feedback_comments', meta: { icon: 'comment' }, fields: [ { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true } }, { field: 'feedback_id', type: 'uuid', meta: { interface: 'select-dropdown' } }, { field: 'user_name', type: 'string' }, { field: 'text', type: 'text' }, { field: 'date_created', type: 'timestamp', schema: { default_value: 'NOW()' } } ] } as any)); } catch (e) { } if (adminPolicy) { console.log('🔐 Granting ALL permissions to Administrator Policy...'); for (const coll of ['visual_feedback', 'visual_feedback_comments']) { for (const action of ['create', 'read', 'update', 'delete']) { try { await client.request(createPermission({ collection: coll, action, fields: ['*'], policy: adminPolicy.id } as any)); } catch (_e) { } } } } console.log('📊 Creating Dashboard...'); try { const dash = await client.request(createDashboard({ name: 'Visual Feedback', icon: 'feedback', color: '#6366f1' })); await client.request(createPanel({ dashboard: dash.id, name: 'Total Feedbacks', type: 'metric', width: 12, height: 6, position_x: 1, position_y: 1, options: { collection: 'visual_feedback', function: 'count', field: 'id' } } as any)); } catch (e) { } console.log('✨ FEEDBACK BOOTSTRAP DONE.'); } catch (e) { console.error('❌ FAILURE:', e); } } setup(); `; const tempFile = path.join(process.cwd(), "temp-bootstrap-feedback.ts"); await fs.writeFile(tempFile, bootstrapScript); try { execSync("npx tsx --env-file=.env " + tempFile, { stdio: "inherit" }); } finally { await fs.remove(tempFile); } }); 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: "16.1.6", react: "^19.0.0", "react-dom": "^19.0.0", "@mintel/next-utils": "workspace:*", "@mintel/next-observability": "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 { 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 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) 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();