Files
at-mintel/packages/cli/src/index.ts
Marc Mintel a8bc039c02
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m33s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build & Push Images (push) Has been skipped
feat: implement centralized Docker base-image strategy and automate registry pushes
2026-02-03 11:50:17 +01:00

423 lines
12 KiB
TypeScript

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 <action> <env>")
.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 <path>")
.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 (
<html lang={locale}>
<body className="antialiased">{children}</body>
</html>
);
}
`,
);
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 (
<main>
<h1>{t('title')} to ${projectName}</h1>
</main>
);
}
`,
);
// 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();