init.js•7.53 kB
import path from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
import { createRequire } from 'module';
import chalk from 'chalk';
import ora from 'ora';
import { ensureDocker } from '../utils/docker.js';
import { ensureProjectDirectory, writeFile } from '../utils/fs.js';
import { DEFAULT_PORTS } from '../config/defaults.js';
const require = createRequire(import.meta.url);
const { version: cliVersion } = require('../../package.json');
const DEFAULT_BACKEND_IMAGE = 'ghcr.io/ultimate-mcp/ultimate-mcp-backend:latest';
const DEFAULT_FRONTEND_IMAGE = 'ghcr.io/ultimate-mcp/ultimate-mcp-frontend:latest';
function generateSecret(prefix, bytes = 24) {
  return `${prefix}_${crypto.randomBytes(bytes).toString('hex')}`;
}
function validatePort(value, label) {
  const numeric = Number.parseInt(value, 10);
  if (!Number.isInteger(numeric) || numeric <= 0 || numeric > 65535) {
    throw new Error(`${label} must be a valid TCP port (1-65535). Received "${value}".`);
  }
  return String(numeric);
}
function resolvePorts(options) {
  return {
    neo4jHttp: options.neo4jHttpPort ? validatePort(options.neo4jHttpPort, 'neo4j-http-port') : DEFAULT_PORTS.neo4jHttp,
    neo4jBolt: options.neo4jBoltPort ? validatePort(options.neo4jBoltPort, 'neo4j-bolt-port') : DEFAULT_PORTS.neo4jBolt,
    backend: options.backendPort ? validatePort(options.backendPort, 'backend-port') : DEFAULT_PORTS.backend,
    frontend: options.frontendPort ? validatePort(options.frontendPort, 'frontend-port') : DEFAULT_PORTS.frontend,
  };
}
function ensureNeo4jPassword(password) {
  if (password.length < 12) {
    throw new Error('Neo4j password must be at least 12 characters for the default password policy.');
  }
  if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
    throw new Error('Neo4j password must include both letters and numbers.');
  }
  if (/\s/.test(password)) {
    throw new Error('Neo4j password cannot contain whitespace characters.');
  }
  return password;
}
function buildComposeContents({ localImages }) {
  const backendSection = localImages
    ? `    build:\n      context: ./backend\n      dockerfile: Dockerfile\n`
    : '    image: ${UMCP_BACKEND_IMAGE}\n';
  const frontendSection = localImages
    ? `    build:\n      context: ./frontend\n      dockerfile: Dockerfile\n`
    : '    image: ${UMCP_FRONTEND_IMAGE}\n';
  return `version: "3.9"
services:
  neo4j:
    image: neo4j:5.23.0
    restart: unless-stopped
    environment:
      - NEO4J_AUTH=neo4j:${'${NEO4J_PASSWORD}'}
    ports:
      - "${'${NEO4J_HTTP_PORT:-7474}'}:7474"
      - "${'${NEO4J_BOLT_PORT:-7687}'}:7687"
    volumes:
      - neo4j_data:/data
      - neo4j_logs:/logs
    healthcheck:
      test: ["CMD-SHELL", "cypher-shell -u neo4j -p ${'${NEO4J_PASSWORD}'} 'RETURN 1'"]
      interval: 15s
      timeout: 10s
      retries: 10
    security_opt:
      - no-new-privileges:true
  backend:
${backendSection}    restart: unless-stopped
    env_file:
      - .env
    environment:
      NEO4J_URI: bolt://neo4j:7687
      NEO4J_USER: neo4j
      NEO4J_PASSWORD: ${'${NEO4J_PASSWORD}'}
      AUTH_TOKEN: ${'${AUTH_TOKEN}'}
      ALLOWED_ORIGINS: http://localhost:${'${FRONTEND_HTTP_PORT:-3000}'}
    ports:
      - "${'${BACKEND_HTTP_PORT:-8000}'}:8000"
    depends_on:
      neo4j:
        condition: service_healthy
    security_opt:
      - no-new-privileges:true
  frontend:
${frontendSection}    restart: unless-stopped
    environment:
      VITE_BACKEND_URL: http://backend:8000
    ports:
      - "${'${FRONTEND_HTTP_PORT:-3000}'}:8080"
    depends_on:
      backend:
        condition: service_started
    security_opt:
      - no-new-privileges:true
volumes:
  neo4j_data:
  neo4j_logs:
`;
}
function buildEnvContents({ authToken, secretKey, neo4jPassword, backendImage, frontendImage, ports }) {
  return `# Generated ${new Date().toISOString()}
# Ultimate MCP deployment secrets
AUTH_TOKEN=${authToken}
SECRET_KEY=${secretKey}
NEO4J_PASSWORD=${neo4jPassword}
NEO4J_HTTP_PORT=${ports.neo4jHttp}
NEO4J_BOLT_PORT=${ports.neo4jBolt}
BACKEND_HTTP_PORT=${ports.backend}
FRONTEND_HTTP_PORT=${ports.frontend}
ALLOWED_ORIGINS=http://localhost:${ports.frontend}
UMCP_BACKEND_IMAGE=${backendImage}
UMCP_FRONTEND_IMAGE=${frontendImage}
`;
}
function buildGitignore() {
  return `# Ultimate MCP deployment
.env
neo4j_data/
neo4j_logs/
`;
}
function buildProjectReadme({ localImages, ports }) {
  const backendUrl = `http://localhost:${ports.backend}`;
  const frontendUrl = `http://localhost:${ports.frontend}`;
  const dockerNote = localImages
    ? `The compose file is configured to build backend/frontend images from local sources.
Ensure the Ultimate MCP repository is checked out with \`backend\` and \`frontend\` folders beside this deployment directory.`
    : 'Docker images are pulled from the published registry. Override via `UMCP_BACKEND_IMAGE` / `UMCP_FRONTEND_IMAGE` in `.env` if needed.';
  return `# Ultimate MCP Deployment
This directory was created by \`ultimate-mcp init\`. Use the commands below to manage the stack:
- \`npx @ultimate-mcp/cli start\` – launch the services
- \`npx @ultimate-mcp/cli stop\` – stop all services
- \`npx @ultimate-mcp/cli upgrade\` – pull the latest images and restart
${dockerNote}
Services:
- Backend API: ${backendUrl}
- Frontend UI: ${frontendUrl}
`;
}
export async function initProject(directory = 'ultimate-mcp', options) {
  ensureDocker();
  const spinner = ora('Scaffolding Ultimate MCP deployment').start();
  const targetDir = path.resolve(process.cwd(), directory);
  await ensureProjectDirectory(targetDir, { force: options.force });
  const backendImage = options.backendImage ?? DEFAULT_BACKEND_IMAGE;
  const frontendImage = options.frontendImage ?? DEFAULT_FRONTEND_IMAGE;
  const localImages = Boolean(options.localImages);
  const ports = resolvePorts(options);
  const neo4jPassword = ensureNeo4jPassword(
    options.neo4jPassword ?? generateSecret('neo4j'),
  );
  const secrets = {
    authToken: generateSecret('umcp_auth'),
    secretKey: generateSecret('umcp_secret'),
    neo4jPassword,
  };
  const composeContents = buildComposeContents({ localImages });
  const envContents = buildEnvContents({ ...secrets, backendImage, frontendImage, ports });
  const readmeContents = buildProjectReadme({ localImages, ports });
  await writeFile(targetDir, 'docker-compose.yml', composeContents);
  await writeFile(targetDir, '.env', envContents, { mode: 0o600 });
  await writeFile(targetDir, '.gitignore', buildGitignore());
  await writeFile(targetDir, 'README.md', readmeContents);
  const config = {
    createdAt: new Date().toISOString(),
    backendImage,
    frontendImage,
    localImages,
    ports,
    machine: os.hostname(),
    cliVersion,
  };
  await writeFile(targetDir, 'ultimate-mcp.json', `${JSON.stringify(config, null, 2)}\n`);
  spinner.succeed('Deployment directory ready');
  console.log(`\n${chalk.green('Next steps:')}`);
  console.log(`  cd ${path.relative(process.cwd(), targetDir) || '.'}`);
  console.log('  npx @ultimate-mcp/cli start');
  console.log(`\nDashboard: http://localhost:${ports.frontend}`);
  console.log(`API docs: http://localhost:${ports.backend}/docs`);
  console.log(`Secrets stored in ${path.join(targetDir, '.env')} (permissions ${chalk.gray('0600')})`);
  if (localImages) {
    console.log(chalk.yellow('\nLocal image mode enabled. Ensure backend/ and frontend/ sources exist beside this directory before running start.'));
  }
  console.log('\n');
}