UNPKG

@gati-framework/cli

Version:

CLI tool for Gati framework - create, develop, build and deploy cloud-native applications

744 lines (643 loc) 18.1 kB
/** * @module cli/utils/file-generator * @description Project scaffolding and file generation utilities */ import { mkdir, writeFile } from 'fs/promises'; import { join } from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import ora from 'ora'; import chalk from 'chalk'; const execAsync = promisify(exec); /** * Generate a new Gati project */ export async function generateProject(options) { const { projectPath, projectName, description, author, template, skipInstall } = options; // Create project directory await mkdir(projectPath, { recursive: true }); // Create directory structure await createDirectoryStructure(projectPath); // Generate files based on template if (template === 'minimal') { await generateMinimalTemplate(projectPath, projectName, description, author); } else { await generateDefaultTemplate(projectPath, projectName, description, author); } // Install dependencies if not skipped if (!skipInstall) { const spinner = ora('Installing dependencies...').start(); try { await execAsync('pnpm install', { cwd: projectPath }); spinner.succeed(chalk.green('Dependencies installed')); } catch (error) { spinner.fail(chalk.red('Failed to install dependencies')); throw error; } } } /** * Create the project directory structure */ async function createDirectoryStructure(projectPath) { const dirs = [ 'src', 'src/handlers', 'src/modules', 'tests', 'tests/unit', 'tests/integration', 'deploy', 'deploy/kubernetes', ]; for (const dir of dirs) { await mkdir(join(projectPath, dir), { recursive: true }); } } /** * Generate default template with example handlers */ async function generateDefaultTemplate(projectPath, projectName, description, author) { // package.json await writeFile(join(projectPath, 'package.json'), JSON.stringify({ name: projectName, version: '0.1.0', type: 'module', description, author, license: 'MIT', scripts: { dev: 'gati dev', build: 'gati build', start: 'node dist/index.js', 'generate:manifests': 'gati generate:manifests', 'generate:types': 'gati generate:types', test: 'vitest', typecheck: 'tsc --noEmit', lint: 'eslint . --ext .ts', }, dependencies: { '@gati-framework/core': '^0.4.5', '@gati-framework/runtime': '^2.0.7', }, devDependencies: { '@gati-framework/cli': '^1.0.14', '@types/node': '^20.10.0', typescript: '^5.3.2', vitest: '^1.0.0', eslint: '^8.54.0', }, engines: { node: '>=18.0.0', }, }, null, 2)); // tsconfig.json await writeFile(join(projectPath, 'tsconfig.json'), JSON.stringify({ extends: '@gati-framework/core/tsconfig.base.json', compilerOptions: { outDir: './dist', rootDir: './src', baseUrl: '.', paths: { '@/*': ['src/*'], }, }, include: ['src/**/*'], exclude: ['node_modules', 'dist', 'tests'], }, null, 2)); // gati.config.ts await writeFile(join(projectPath, 'gati.config.ts'), `/** * @file Gati configuration */ export default { port: 3000, handlers: './src/handlers', modules: './src/modules', }; `); // README.md await writeFile(join(projectPath, 'README.md'), `# ${projectName} ${description} ## Getting Started \`\`\`bash # Install dependencies pnpm install # Start development server pnpm dev # Build for production pnpm build # Run tests pnpm test \`\`\` ## Project Structure \`\`\` ${projectName}/ ├── src/ │ ├── handlers/ # HTTP request handlers │ │ ├── hello.ts # Example handler │ │ └── health.ts # Health check endpoint │ ├── modules/ # Reusable modules │ └── index.ts # Application entry point ├── deploy/ │ └── kubernetes/ # Kubernetes manifests │ ├── deployment.yaml │ └── service.yaml ├── tests/ │ ├── unit/ # Unit tests │ └── integration/ # Integration tests ├── Dockerfile # Production Docker image ├── docker-compose.yml # Local Docker setup ├── gati.config.ts # Gati configuration └── package.json \`\`\` ## Development ### Local Development \`\`\`bash # Run with hot reload pnpm dev # Type checking pnpm typecheck # Linting pnpm lint \`\`\` ### Environment Variables Copy \`.env.example\` to \`.env\` and configure: \`\`\`bash cp .env.example .env \`\`\` ## Deployment ### Docker \`\`\`bash # Build Docker image docker build -t ${projectName}:latest . # Run container docker run -p 3000:3000 ${projectName}:latest # Or use docker-compose docker-compose up \`\`\` ### Kubernetes \`\`\`bash # Apply manifests kubectl apply -f deploy/kubernetes/ # Check deployment kubectl get pods kubectl get services # View logs kubectl logs -f deployment/${projectName} \`\`\` ### Production Build \`\`\`bash # Build TypeScript pnpm build # Run production server pnpm start \`\`\` ## API Endpoints - \`GET /health\` - Health check endpoint - \`GET /api/hello?name=<name>\` - Example hello endpoint ## Learn More - [Gati Documentation](https://github.com/krishnapaul242/gati) - [Handler Guide](https://github.com/krishnapaul242/gati/blob/main/docs/handlers.md) - [Module Guide](https://github.com/krishnapaul242/gati/blob/main/docs/modules.md) ## License MIT `); // Example handler and entrypoint await writeFile(join(projectPath, 'src/handlers/hello.ts'), `/** * @handler GET /hello * @description Simple hello world handler */ import type { Handler } from '@gati-framework/runtime'; export const handler: Handler = (req, res) => { const name = req.query.name || 'World'; res.json({ message: \`Hello, \${name}!\`, timestamp: new Date().toISOString(), }); }; `); await writeFile(join(projectPath, 'src/index.ts'), `import { createApp, loadHandlers } from '@gati-framework/runtime'; async function main() { const app = createApp({ port: Number(process.env['PORT']) || 3000, host: process.env['HOST'] || '0.0.0.0' }); await loadHandlers(app, './src/handlers', { basePath: '/api', verbose: true }); await app.listen(); console.log(\`Server running on \${app.getConfig().host}:\${app.getConfig().port}\`); // Graceful shutdown const shutdown = async (signal: string) => { console.log(\`\${signal} received, shutting down gracefully...\`); await app.close(); process.exit(0); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } main().catch((err) => { console.error('Failed to start app', err); process.exit(1); }); `); // .gitignore await writeFile(join(projectPath, '.gitignore'), `# Dependencies node_modules/ .pnpm-store/ # Build output dist/ build/ *.tsbuildinfo # Environment variables .env .env.local .env.*.local # Logs logs/ *.log npm-debug.log* pnpm-debug.log* # Testing coverage/ .nyc_output/ # IDE .vscode/ .idea/ *.swp *.swo *~ # OS .DS_Store Thumbs.db `); // .dockerignore await writeFile(join(projectPath, '.dockerignore'), `node_modules/ .pnpm-store/ dist/ build/ .env .env.* *.log coverage/ .git/ .github/ tests/ *.md .vscode/ .idea/ .DS_Store Thumbs.db `); // .env.example await writeFile(join(projectPath, '.env.example'), `# Server Configuration PORT=3000 HOST=0.0.0.0 NODE_ENV=development # Application Configuration APP_NAME=${projectName} APP_VERSION=0.1.0 # Logging LOG_LEVEL=info LOG_PRETTY=true `); // .npmrc - Disable workspace features for standalone project await writeFile(join(projectPath, '.npmrc'), `# Standalone project - not part of a workspace workspace-root=false shamefully-hoist=false `); // Dockerfile await writeFile(join(projectPath, 'Dockerfile'), `# Multi-stage Dockerfile for ${projectName} # Built with Gati framework # Build stage FROM node:20-alpine AS builder WORKDIR /app # Install build dependencies RUN apk add --no-cache python3 make g++ # Install pnpm RUN npm install -g pnpm@8 # Copy package files COPY package*.json ./ COPY pnpm-lock.yaml* ./ # Install dependencies RUN pnpm install --frozen-lockfile # Copy source code COPY . . # Build application RUN pnpm build # Production stage FROM node:20-alpine WORKDIR /app # Install tini for proper signal handling RUN apk add --no-cache tini # Create non-root user RUN addgroup -g 1001 -S gati && \\ adduser -S gati -u 1001 # Install pnpm RUN npm install -g pnpm@8 # Copy package files COPY package*.json ./ COPY pnpm-lock.yaml* ./ # Install production dependencies only RUN pnpm install --prod --frozen-lockfile # Copy built application from builder COPY --from=builder /app/dist ./dist # Set ownership RUN chown -R gati:gati /app # Switch to non-root user USER gati # Expose port EXPOSE 3000 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" # Use tini for proper signal handling ENTRYPOINT ["/sbin/tini", "--"] # Start application CMD ["node", "dist/index.js"] `); // docker-compose.yml await writeFile(join(projectPath, 'docker-compose.yml'), `version: '3.8' services: app: build: context: . dockerfile: Dockerfile ports: - "\${PORT:-3000}:3000" environment: - NODE_ENV=\${NODE_ENV:-development} - PORT=3000 - HOST=0.0.0.0 volumes: # Mount source code for development (comment out for production) - ./src:/app/src - ./package.json:/app/package.json restart: unless-stopped healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] interval: 30s timeout: 10s retries: 3 start_period: 60s networks: default: name: ${projectName}-network `); // Kubernetes deployment await writeFile(join(projectPath, 'deploy/kubernetes/deployment.yaml'), `apiVersion: apps/v1 kind: Deployment metadata: name: ${projectName} labels: app: ${projectName} version: v1 spec: replicas: 2 selector: matchLabels: app: ${projectName} strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: ${projectName} version: v1 spec: containers: - name: ${projectName} image: ${projectName}:latest imagePullPolicy: IfNotPresent ports: - containerPort: 3000 name: http protocol: TCP env: - name: NODE_ENV value: "production" - name: PORT value: "3000" - name: HOST value: "0.0.0.0" livenessProbe: httpGet: path: /health port: 3000 scheme: HTTP initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 readinessProbe: httpGet: path: /health port: 3000 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 resources: limits: cpu: 500m memory: 512Mi requests: cpu: 250m memory: 256Mi securityContext: runAsNonRoot: true runAsUser: 1001 allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false restartPolicy: Always securityContext: fsGroup: 1001 runAsUser: 1001 runAsNonRoot: true `); // Kubernetes service await writeFile(join(projectPath, 'deploy/kubernetes/service.yaml'), `apiVersion: v1 kind: Service metadata: name: ${projectName} labels: app: ${projectName} spec: type: ClusterIP selector: app: ${projectName} ports: - name: http port: 80 targetPort: 3000 protocol: TCP sessionAffinity: None `); // Add health handler await writeFile(join(projectPath, 'src/handlers/health.ts'), `/** * @handler GET /health * @description Health check endpoint for monitoring and orchestration */ import type { Handler } from '@gati-framework/runtime'; export const handler: Handler = (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), }); }; `); } /** * Generate minimal template */ async function generateMinimalTemplate(projectPath, projectName, description, author) { // Similar to default but without example files await writeFile(join(projectPath, 'package.json'), JSON.stringify({ name: projectName, version: '0.1.0', type: 'module', description, author, license: 'MIT', scripts: { dev: 'gati dev', build: 'gati build', start: 'node dist/index.js', test: 'vitest', typecheck: 'tsc --noEmit', }, dependencies: { '@gati-framework/core': '^0.4.5', '@gati-framework/runtime': '^2.0.7', }, devDependencies: { '@gati-framework/cli': '^1.0.14', '@types/node': '^20.10.0', typescript: '^5.3.2', }, engines: { node: '>=18.0.0', }, }, null, 2)); await writeFile(join(projectPath, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler', outDir: './dist', rootDir: './src', strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, baseUrl: '.', paths: { '@/*': ['src/*'] }, }, include: ['src/**/*'], exclude: ['node_modules', 'dist'], }, null, 2)); await writeFile(join(projectPath, 'gati.config.ts'), `export default { port: 3000, handlers: './src/handlers', }; `); await writeFile(join(projectPath, 'README.md'), `# ${projectName} ${description} ## Getting Started \`\`\`bash pnpm install pnpm dev \`\`\` ## Runtime Entry This template expects a runtime entry at \`src/index.ts\`: \`\`\`ts import { createApp, loadHandlers } from '@gati-framework/runtime'; async function main() { const app = createApp({ port: 3000 }); await loadHandlers(app, './src/handlers'); await app.listen(); } main(); \`\`\` `); await writeFile(join(projectPath, 'src/index.ts'), `import { createApp, loadHandlers } from '@gati-framework/runtime'; async function main() { const app = createApp({ port: Number(process.env['PORT']) || 3000, host: process.env['HOST'] || '0.0.0.0' }); await loadHandlers(app, './src/handlers'); await app.listen(); // Graceful shutdown const shutdown = async (signal: string) => { console.log(\`\${signal} received, shutting down...\`); await app.close(); process.exit(0); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } main().catch((err) => { console.error(err); process.exit(1); }); `); await writeFile(join(projectPath, '.gitignore'), `node_modules/ dist/ .env *.log coverage/ .DS_Store `); // Add basic deployment files for minimal template too await writeFile(join(projectPath, '.env.example'), `PORT=3000 NODE_ENV=development `); // .npmrc - Disable workspace features for standalone project await writeFile(join(projectPath, '.npmrc'), `# Standalone project - not part of a workspace workspace-root=false shamefully-hoist=false `); await writeFile(join(projectPath, 'Dockerfile'), `# Minimal Dockerfile for ${projectName} FROM node:20-alpine AS builder WORKDIR /app RUN npm install -g pnpm@8 COPY package*.json pnpm-lock.yaml* ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build FROM node:20-alpine WORKDIR /app RUN apk add --no-cache tini && \\ npm install -g pnpm@8 && \\ addgroup -g 1001 -S gati && \\ adduser -S gati -u 1001 COPY package*.json pnpm-lock.yaml* ./ RUN pnpm install --prod --frozen-lockfile COPY --from=builder /app/dist ./dist RUN chown -R gati:gati /app USER gati EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "dist/index.js"] `); // Add health handler for minimal template await writeFile(join(projectPath, 'src/handlers/health.ts'), `import type { Handler } from '@gati-framework/runtime'; export const handler: Handler = (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }; `); } //# sourceMappingURL=file-generator.js.map