Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 14s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
543 lines
17 KiB
JavaScript
543 lines
17 KiB
JavaScript
#!/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 <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: "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 (
|
|
<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)
|
|
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();
|