Ver código fonte

feat: Implement comprehensive application deployment system

This commit implements a complete deployment system that allows users to deploy
applications from Git repositories with support for multiple application types.

Backend Changes:
- Add deployment service with Docker container orchestration
- Implement git-based deployment workflow
- Add support for npm, React, Next.js, PHP, Python, static, and Docker apps
- Create nginx configuration service for dynamic proxy routing
- Add deployment endpoints: deploy, stop, logs, deployments list
- Update applications table schema with app_type, port, container_id fields
- Install dockerode for Docker API integration
- Configure API service for Docker-in-Docker with socket access

Dashboard Changes:
- Create DeployApplicationModal with comprehensive deployment form
- Add application type selector with 7 supported types
- Implement stop/start application controls
- Update API service with deployment endpoints
- Add deployment logs and container logs support
- Improve Applications page UI with deploy/stop actions

Infrastructure Changes:
- Add Docker socket volume mount to API service
- Enable privileged mode for container management
- Create dynamic nginx proxy configuration system
- Add apps-proxy.conf for application routing
- Support slug-based and custom domain routing

Features:
- Deploy applications from Git repositories
- Auto-generate Dockerfiles based on app type
- Environment variable configuration
- Custom domain support
- Build and deployment logging
- Container lifecycle management
- Dynamic nginx reverse proxy configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
fszontagh 3 meses atrás
pai
commit
dd679a4d4d

+ 398 - 0
dashboard/src/components/DeployApplicationModal.tsx

@@ -0,0 +1,398 @@
+import { useState } from 'react';
+import { X, GitBranch, Package, Terminal, Globe } from 'lucide-react';
+import { useMutation, useQueryClient } from 'react-query';
+import apiService from '@/services/api';
+import toast from 'react-hot-toast';
+
+interface DeployApplicationModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+const APP_TYPES = [
+  { value: 'npm', label: 'Node.js / npm', icon: '📦' },
+  { value: 'react', label: 'React', icon: '⚛️' },
+  { value: 'nextjs', label: 'Next.js', icon: '▲' },
+  { value: 'php', label: 'PHP', icon: '🐘' },
+  { value: 'python', label: 'Python', icon: '🐍' },
+  { value: 'static', label: 'Static HTML', icon: '📄' },
+  { value: 'docker', label: 'Docker (Custom)', icon: '🐳' },
+];
+
+export default function DeployApplicationModal({ isOpen, onClose }: DeployApplicationModalProps) {
+  const queryClient = useQueryClient();
+  const [formData, setFormData] = useState({
+    name: '',
+    slug: '',
+    description: '',
+    appType: 'npm',
+    repositoryUrl: '',
+    branch: 'main',
+    buildCommand: '',
+    startCommand: '',
+    port: 3000,
+    domains: '',
+    environment: '',
+  });
+
+  const createMutation = useMutation(
+    (data: any) => apiService.createApplication(data),
+    {
+      onSuccess: () => {
+        toast.success('Application created successfully');
+        queryClient.invalidateQueries('applications');
+        onClose();
+        resetForm();
+      },
+      onError: (error: any) => {
+        toast.error(error.response?.data?.error || 'Failed to create application');
+      },
+    }
+  );
+
+  const resetForm = () => {
+    setFormData({
+      name: '',
+      slug: '',
+      description: '',
+      appType: 'npm',
+      repositoryUrl: '',
+      branch: 'main',
+      buildCommand: '',
+      startCommand: '',
+      port: 3000,
+      domains: '',
+      environment: '',
+    });
+  };
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+
+    // Parse environment variables
+    const envObject: Record<string, string> = {};
+    if (formData.environment) {
+      formData.environment.split('\n').forEach((line) => {
+        const [key, value] = line.split('=');
+        if (key && value) {
+          envObject[key.trim()] = value.trim();
+        }
+      });
+    }
+
+    // Parse domains
+    const domainsArray = formData.domains
+      ? formData.domains.split(',').map((d) => d.trim()).filter(Boolean)
+      : [];
+
+    createMutation.mutate({
+      ...formData,
+      environment: envObject,
+      domains: domainsArray,
+    });
+  };
+
+  const handleInputChange = (field: string, value: any) => {
+    setFormData((prev) => ({ ...prev, [field]: value }));
+
+    // Auto-generate slug from name
+    if (field === 'name' && !formData.slug) {
+      const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
+      setFormData((prev) => ({ ...prev, slug }));
+    }
+  };
+
+  const getDefaultCommands = () => {
+    switch (formData.appType) {
+      case 'npm':
+        return {
+          buildCommand: 'npm install',
+          startCommand: 'npm start',
+        };
+      case 'react':
+        return {
+          buildCommand: 'npm install && npm run build',
+          startCommand: 'npm run preview',
+        };
+      case 'nextjs':
+        return {
+          buildCommand: 'npm install && npm run build',
+          startCommand: 'npm start',
+        };
+      case 'php':
+        return {
+          buildCommand: '',
+          startCommand: '',
+        };
+      case 'python':
+        return {
+          buildCommand: 'pip install -r requirements.txt',
+          startCommand: 'python app.py',
+        };
+      case 'static':
+        return {
+          buildCommand: '',
+          startCommand: '',
+        };
+      case 'docker':
+        return {
+          buildCommand: 'docker build -t app .',
+          startCommand: 'docker run -p 3000:3000 app',
+        };
+      default:
+        return { buildCommand: '', startCommand: '' };
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 overflow-y-auto">
+      <div className="flex min-h-screen items-center justify-center p-4">
+        <div className="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity" onClick={onClose} />
+
+        <div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
+          <div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between">
+            <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
+              Deploy New Application
+            </h3>
+            <button
+              onClick={onClose}
+              className="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
+            >
+              <X className="h-6 w-6" />
+            </button>
+          </div>
+
+          <form onSubmit={handleSubmit} className="p-6 space-y-6">
+            {/* Basic Info */}
+            <div className="space-y-4">
+              <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center">
+                <Package className="h-4 w-4 mr-2" />
+                Application Details
+              </h4>
+
+              <div className="grid grid-cols-2 gap-4">
+                <div>
+                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                    Name *
+                  </label>
+                  <input
+                    type="text"
+                    required
+                    value={formData.name}
+                    onChange={(e) => handleInputChange('name', e.target.value)}
+                    className="input mt-1"
+                    placeholder="My App"
+                  />
+                </div>
+
+                <div>
+                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                    Slug *
+                  </label>
+                  <input
+                    type="text"
+                    required
+                    value={formData.slug}
+                    onChange={(e) => handleInputChange('slug', e.target.value)}
+                    className="input mt-1"
+                    placeholder="my-app"
+                  />
+                </div>
+              </div>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                  Description
+                </label>
+                <textarea
+                  value={formData.description}
+                  onChange={(e) => handleInputChange('description', e.target.value)}
+                  className="input mt-1"
+                  rows={3}
+                  placeholder="Brief description of your application"
+                />
+              </div>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                  Application Type *
+                </label>
+                <div className="grid grid-cols-4 gap-2">
+                  {APP_TYPES.map((type) => (
+                    <button
+                      key={type.value}
+                      type="button"
+                      onClick={() => {
+                        handleInputChange('appType', type.value);
+                        const defaults = getDefaultCommands();
+                        setFormData((prev) => ({
+                          ...prev,
+                          appType: type.value,
+                          buildCommand: prev.buildCommand || defaults.buildCommand,
+                          startCommand: prev.startCommand || defaults.startCommand,
+                        }));
+                      }}
+                      className={`p-3 rounded-lg border-2 transition-colors ${
+                        formData.appType === type.value
+                          ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
+                          : 'border-gray-200 dark:border-gray-700 hover:border-primary-300 dark:hover:border-primary-700'
+                      }`}
+                    >
+                      <div className="text-2xl mb-1">{type.icon}</div>
+                      <div className="text-xs text-gray-900 dark:text-gray-100">{type.label}</div>
+                    </button>
+                  ))}
+                </div>
+              </div>
+            </div>
+
+            {/* Git Repository */}
+            <div className="space-y-4">
+              <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center">
+                <GitBranch className="h-4 w-4 mr-2" />
+                Git Repository
+              </h4>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                  Repository URL *
+                </label>
+                <input
+                  type="url"
+                  required
+                  value={formData.repositoryUrl}
+                  onChange={(e) => handleInputChange('repositoryUrl', e.target.value)}
+                  className="input mt-1"
+                  placeholder="https://github.com/username/repo.git"
+                />
+              </div>
+
+              <div className="grid grid-cols-2 gap-4">
+                <div>
+                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                    Branch
+                  </label>
+                  <input
+                    type="text"
+                    value={formData.branch}
+                    onChange={(e) => handleInputChange('branch', e.target.value)}
+                    className="input mt-1"
+                    placeholder="main"
+                  />
+                </div>
+
+                <div>
+                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                    Port
+                  </label>
+                  <input
+                    type="number"
+                    value={formData.port}
+                    onChange={(e) => handleInputChange('port', parseInt(e.target.value))}
+                    className="input mt-1"
+                    placeholder="3000"
+                  />
+                </div>
+              </div>
+            </div>
+
+            {/* Build & Deploy */}
+            <div className="space-y-4">
+              <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center">
+                <Terminal className="h-4 w-4 mr-2" />
+                Build & Deploy Commands
+              </h4>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                  Build Command
+                </label>
+                <input
+                  type="text"
+                  value={formData.buildCommand}
+                  onChange={(e) => handleInputChange('buildCommand', e.target.value)}
+                  className="input mt-1 font-mono text-sm"
+                  placeholder="npm install && npm run build"
+                />
+              </div>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                  Start Command
+                </label>
+                <input
+                  type="text"
+                  value={formData.startCommand}
+                  onChange={(e) => handleInputChange('startCommand', e.target.value)}
+                  className="input mt-1 font-mono text-sm"
+                  placeholder="npm start"
+                />
+              </div>
+            </div>
+
+            {/* Environment & Domains */}
+            <div className="space-y-4">
+              <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center">
+                <Globe className="h-4 w-4 mr-2" />
+                Configuration
+              </h4>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                  Environment Variables
+                </label>
+                <textarea
+                  value={formData.environment}
+                  onChange={(e) => handleInputChange('environment', e.target.value)}
+                  className="input mt-1 font-mono text-sm"
+                  rows={4}
+                  placeholder="NODE_ENV=production&#10;API_KEY=your-api-key&#10;DATABASE_URL=postgresql://..."
+                />
+                <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
+                  One per line, format: KEY=value
+                </p>
+              </div>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                  Custom Domains
+                </label>
+                <input
+                  type="text"
+                  value={formData.domains}
+                  onChange={(e) => handleInputChange('domains', e.target.value)}
+                  className="input mt-1"
+                  placeholder="app.example.com, www.example.com"
+                />
+                <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
+                  Comma-separated list of domains
+                </p>
+              </div>
+            </div>
+
+            {/* Actions */}
+            <div className="flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700">
+              <button
+                type="button"
+                onClick={onClose}
+                className="btn-secondary"
+                disabled={createMutation.isLoading}
+              >
+                Cancel
+              </button>
+              <button
+                type="submit"
+                className="btn-primary"
+                disabled={createMutation.isLoading}
+              >
+                {createMutation.isLoading ? 'Creating...' : 'Create & Deploy'}
+              </button>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 55 - 18
dashboard/src/pages/Applications.tsx

@@ -9,11 +9,13 @@ import {
   GitBranch,
   Settings,
   MoreHorizontal,
+  Square,
 } from 'lucide-react';
 import { format } from 'date-fns';
 import { useNavigate } from 'react-router-dom';
 import apiService from '@/services/api';
 import DataTable from '@/components/DataTable';
+import DeployApplicationModal from '@/components/DeployApplicationModal';
 import { Application } from '@/types';
 import toast from 'react-hot-toast';
 
@@ -77,6 +79,7 @@ const columns = [
 export default function Applications() {
   const [page, setPage] = useState(1);
   const [limit, setLimit] = useState(20);
+  const [showDeployModal, setShowDeployModal] = useState(false);
   const navigate = useNavigate();
   const queryClient = useQueryClient();
 
@@ -89,12 +92,28 @@ export default function Applications() {
   );
 
   const deployMutation = useMutation(
-    (appId: string) => apiService.deployApplication(appId, { version: 'v1.0.0' }),
+    (appId: string) => apiService.deployApplication(appId),
     {
       onSuccess: () => {
         toast.success('Deployment started successfully');
         queryClient.invalidateQueries('applications');
       },
+      onError: (error: any) => {
+        toast.error(error.response?.data?.error || 'Failed to start deployment');
+      },
+    }
+  );
+
+  const stopMutation = useMutation(
+    (appId: string) => apiService.stopApplication(appId),
+    {
+      onSuccess: () => {
+        toast.success('Application stopped successfully');
+        queryClient.invalidateQueries('applications');
+      },
+      onError: (error: any) => {
+        toast.error(error.response?.data?.error || 'Failed to stop application');
+      },
     }
   );
 
@@ -105,6 +124,9 @@ export default function Applications() {
         toast.success('Application deleted successfully');
         queryClient.invalidateQueries('applications');
       },
+      onError: (error: any) => {
+        toast.error(error.response?.data?.error || 'Failed to delete application');
+      },
     }
   );
 
@@ -114,6 +136,12 @@ export default function Applications() {
     }
   };
 
+  const handleStop = (app: Application) => {
+    if (window.confirm(`Are you sure you want to stop "${app.name}"?`)) {
+      stopMutation.mutate(app.id);
+    }
+  };
+
   const handleDelete = (app: Application) => {
     if (window.confirm(`Are you sure you want to delete application "${app.name}"?`)) {
       deleteApplicationMutation.mutate(app.id);
@@ -136,21 +164,25 @@ export default function Applications() {
       >
         <GitBranch className="h-4 w-4" />
       </button>
-      <button
-        onClick={() => navigate(`/applications/${app.id}/settings`)}
-        className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
-        title="Settings"
-      >
-        <Settings className="h-4 w-4" />
-      </button>
-      <button
-        onClick={() => handleDeploy(app)}
-        disabled={app.status === 'building' || deployMutation.isLoading}
-        className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 disabled:opacity-50"
-        title="Deploy"
-      >
-        <Play className="h-4 w-4" />
-      </button>
+      {app.status === 'active' ? (
+        <button
+          onClick={() => handleStop(app)}
+          disabled={stopMutation.isLoading}
+          className="text-orange-600 dark:text-orange-400 hover:text-orange-900 dark:hover:text-orange-300 disabled:opacity-50"
+          title="Stop"
+        >
+          <Square className="h-4 w-4" />
+        </button>
+      ) : (
+        <button
+          onClick={() => handleDeploy(app)}
+          disabled={app.status === 'building' || deployMutation.isLoading}
+          className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 disabled:opacity-50"
+          title="Deploy"
+        >
+          <Play className="h-4 w-4" />
+        </button>
+      )}
       <button
         onClick={() => handleDelete(app)}
         className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
@@ -171,14 +203,19 @@ export default function Applications() {
           </p>
         </div>
         <button
-          onClick={() => navigate('/applications/create')}
+          onClick={() => setShowDeployModal(true)}
           className="btn-primary flex items-center"
         >
           <Plus className="mr-2 h-4 w-4" />
-          Create Application
+          Deploy Application
         </button>
       </div>
 
+      <DeployApplicationModal
+        isOpen={showDeployModal}
+        onClose={() => setShowDeployModal(false)}
+      />
+
       <DataTable
         data={data?.data || []}
         columns={columns}

+ 18 - 3
dashboard/src/services/api.ts

@@ -160,13 +160,28 @@ class ApiService {
     return response.data;
   }
 
-  async deployApplication(appId: string, deploymentData: any) {
-    const response = await this.api.post(`/deployments`, { applicationId: appId, ...deploymentData });
+  async deployApplication(appId: string, deploymentData?: any) {
+    const response = await this.api.post(`/applications/${appId}/deploy`, deploymentData || {});
+    return response.data;
+  }
+
+  async stopApplication(appId: string) {
+    const response = await this.api.post(`/applications/${appId}/stop`);
     return response.data;
   }
 
   async getDeployments(applicationId: string, page = 1, limit = 20) {
-    const response = await this.api.get(`/deployments?applicationId=${applicationId}&page=${page}&limit=${limit}`);
+    const response = await this.api.get(`/applications/${applicationId}/deployments?page=${page}&limit=${limit}`);
+    return response.data;
+  }
+
+  async getDeploymentLogs(appId: string, deploymentId: string) {
+    const response = await this.api.get(`/applications/${appId}/deployments/${deploymentId}/logs`);
+    return response.data;
+  }
+
+  async getContainerLogs(appId: string) {
+    const response = await this.api.get(`/applications/${appId}/logs`);
     return response.data;
   }
 

+ 2 - 0
docker-compose.yml

@@ -103,6 +103,7 @@ services:
       - ./services/api:/app
       - /app/node_modules
       - ./apps:/apps
+      - /var/run/docker.sock:/var/run/docker.sock
     networks:
       - saas-network
     depends_on:
@@ -110,6 +111,7 @@ services:
       - redis
       - auth-service
     restart: unless-stopped
+    privileged: true
 
   # Storage Service (MinIO for S3-compatible storage)
   storage:

+ 1 - 0
nginx/conf.d/apps-proxy.conf

@@ -0,0 +1 @@
+# Auto-generated application proxy configuration

+ 2 - 8
nginx/conf.d/default.conf

@@ -67,14 +67,8 @@ server {
         add_header Content-Type text/plain;
     }
 
-    # Static files and deployed apps
-    location /apps/ {
-        proxy_pass http://api_service;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto $scheme;
-    }
+    # Include dynamic application proxy configurations
+    include /etc/nginx/conf.d/apps-proxy.conf;
 
     # Storage routes (for file uploads/downloads)
     location /storage/ {

+ 625 - 2
package-lock.json

@@ -809,6 +809,12 @@
         "node": ">=18.0.0"
       }
     },
+    "node_modules/@balena/dockerignore": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
+      "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
+      "license": "Apache-2.0"
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.25.12",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1251,6 +1257,65 @@
         "node": ">=18"
       }
     },
+    "node_modules/@grpc/grpc-js": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz",
+      "integrity": "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@grpc/proto-loader": "^0.8.0",
+        "@js-sdsl/ordered-map": "^4.4.2"
+      },
+      "engines": {
+        "node": ">=12.10.0"
+      }
+    },
+    "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
+      "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "lodash.camelcase": "^4.3.0",
+        "long": "^5.0.0",
+        "protobufjs": "^7.5.3",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@grpc/proto-loader": {
+      "version": "0.7.15",
+      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+      "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "lodash.camelcase": "^4.3.0",
+        "long": "^5.0.0",
+        "protobufjs": "^7.2.5",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@js-sdsl/ordered-map": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+      "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/js-sdsl"
+      }
+    },
     "node_modules/@mapbox/node-pre-gyp": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -1271,6 +1336,70 @@
         "node-pre-gyp": "bin/node-pre-gyp"
       }
     },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/@redis/bloom": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
@@ -2011,6 +2140,27 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/docker-modem": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
+      "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/ssh2": "*"
+      }
+    },
+    "node_modules/@types/dockerode": {
+      "version": "3.3.47",
+      "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.47.tgz",
+      "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/docker-modem": "*",
+        "@types/node": "*",
+        "@types/ssh2": "*"
+      }
+    },
     "node_modules/@types/express": {
       "version": "4.17.25",
       "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
@@ -2083,7 +2233,6 @@
       "version": "20.19.25",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
       "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "undici-types": "~6.21.0"
@@ -2166,6 +2315,30 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/ssh2": {
+      "version": "1.15.5",
+      "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
+      "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^18.11.18"
+      }
+    },
+    "node_modules/@types/ssh2/node_modules/@types/node": {
+      "version": "18.19.130",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+      "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/ssh2/node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "license": "MIT"
+    },
     "node_modules/@types/uuid": {
       "version": "9.0.8",
       "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
@@ -2253,6 +2426,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
     "node_modules/append-field": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
@@ -2285,6 +2473,15 @@
       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
       "license": "MIT"
     },
+    "node_modules/asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
     "node_modules/async": {
       "version": "3.2.6",
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -2329,6 +2526,26 @@
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "license": "MIT"
     },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
     "node_modules/bcrypt": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@@ -2343,12 +2560,32 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
     "node_modules/bcryptjs": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
       "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
       "license": "MIT"
     },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
     "node_modules/block-stream2": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
@@ -2405,6 +2642,30 @@
       "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
       "license": "MIT"
     },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
     "node_modules/buffer-crc32": {
       "version": "0.2.13",
       "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -2426,6 +2687,15 @@
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
       "license": "MIT"
     },
+    "node_modules/buildcheck": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
+      "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
+      "optional": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/busboy": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -2502,6 +2772,20 @@
         "node": ">=10"
       }
     },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/cluster-key-slot": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -2511,6 +2795,24 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
     "node_modules/color-support": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -2644,6 +2946,20 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/cpu-features": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
+      "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
+      "hasInstallScript": true,
+      "optional": true,
+      "dependencies": {
+        "buildcheck": "~0.0.6",
+        "nan": "^2.19.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -2722,6 +3038,75 @@
         "node": ">=8"
       }
     },
+    "node_modules/docker-modem": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
+      "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "readable-stream": "^3.5.0",
+        "split-ca": "^1.0.1",
+        "ssh2": "^1.15.0"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/docker-modem/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/docker-modem/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/dockerode": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.9.tgz",
+      "integrity": "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@balena/dockerignore": "^1.0.2",
+        "@grpc/grpc-js": "^1.11.1",
+        "@grpc/proto-loader": "^0.7.13",
+        "docker-modem": "^5.0.6",
+        "protobufjs": "^7.3.2",
+        "tar-fs": "^2.1.4",
+        "uuid": "^10.0.0"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/dockerode/node_modules/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/dotenv": {
       "version": "16.6.1",
       "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -2778,6 +3163,15 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
     "node_modules/es-define-property": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2865,6 +3259,15 @@
         "@esbuild/win32-x64": "0.25.12"
       }
     },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/escape-html": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -3073,6 +3476,12 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "license": "MIT"
+    },
     "node_modules/fs-minipass": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -3166,6 +3575,15 @@
         "node": ">= 4"
       }
     },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3379,6 +3797,26 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
     "node_modules/inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -3561,6 +3999,12 @@
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
       "license": "MIT"
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT"
+    },
     "node_modules/lodash.includes": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -3603,6 +4047,12 @@
       "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
       "license": "MIT"
     },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
     "node_modules/make-dir": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -3797,6 +4247,12 @@
         "mkdirp": "bin/cmd.js"
       }
     },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+      "license": "MIT"
+    },
     "node_modules/ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -3822,6 +4278,13 @@
         "node": ">= 6.0.0"
       }
     },
+    "node_modules/nan": {
+      "version": "2.23.1",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz",
+      "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==",
+      "license": "MIT",
+      "optional": true
+    },
     "node_modules/negotiator": {
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -4125,6 +4588,30 @@
       "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
       "license": "MIT"
     },
+    "node_modules/protobufjs": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+      "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -4144,6 +4631,16 @@
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
     "node_modules/qs": {
       "version": "6.13.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -4232,6 +4729,15 @@
         "@redis/time-series": "1.1.0"
       }
     },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/resolve-pkg-maps": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -4480,6 +4986,12 @@
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
       "license": "ISC"
     },
+    "node_modules/split-ca": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
+      "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
+      "license": "ISC"
+    },
     "node_modules/split-on-first": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -4498,6 +5010,23 @@
         "node": ">= 10.x"
       }
     },
+    "node_modules/ssh2": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
+      "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "asn1": "^0.2.6",
+        "bcrypt-pbkdf": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      },
+      "optionalDependencies": {
+        "cpu-features": "~0.0.10",
+        "nan": "^2.23.0"
+      }
+    },
     "node_modules/statuses": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -4588,6 +5117,40 @@
         "node": ">=10"
       }
     },
+    "node_modules/tar-fs": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+      "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.1.4"
+      }
+    },
+    "node_modules/tar-fs/node_modules/chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "license": "ISC"
+    },
+    "node_modules/tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/tar/node_modules/mkdirp": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -4651,6 +5214,12 @@
         "fsevents": "~2.3.3"
       }
     },
+    "node_modules/tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
+      "license": "Unlicense"
+    },
     "node_modules/type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -4688,7 +5257,6 @@
       "version": "6.21.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/unpipe": {
@@ -4817,6 +5385,23 @@
         "string-width": "^1.0.2 || 2 || 3 || 4"
       }
     },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -4881,19 +5466,57 @@
         "node": ">=0.4"
       }
     },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "license": "ISC"
     },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "services/api": {
       "name": "@saas/api-service",
       "version": "1.0.0",
       "dependencies": {
+        "@types/dockerode": "^3.3.47",
         "axios": "^1.6.2",
         "bcrypt": "^5.1.1",
         "cors": "^2.8.5",
+        "dockerode": "^4.0.9",
         "dotenv": "^16.3.1",
         "express": "^4.18.2",
         "express-rate-limit": "^7.1.5",

+ 2 - 0
services/api/package.json

@@ -11,9 +11,11 @@
     "seed": "node dist/seed.js"
   },
   "dependencies": {
+    "@types/dockerode": "^3.3.47",
     "axios": "^1.6.2",
     "bcrypt": "^5.1.1",
     "cors": "^2.8.5",
+    "dockerode": "^4.0.9",
     "dotenv": "^16.3.1",
     "express": "^4.18.2",
     "express-rate-limit": "^7.1.5",

+ 182 - 5
services/api/src/routes/applications.ts

@@ -1,5 +1,6 @@
 import express from 'express';
 import { pool } from '../index';
+import { deploymentService } from '../services/deployment';
 
 const router = express.Router();
 
@@ -38,13 +39,29 @@ router.get('/', async (req: any, res) => {
 router.post('/', async (req: any, res) => {
   try {
     const userId = req.user.userId;
-    const { name, slug, description, repositoryUrl, buildCommand, startCommand, environment } = req.body;
+    const {
+      name,
+      slug,
+      description,
+      appType,
+      repositoryUrl,
+      branch,
+      buildCommand,
+      startCommand,
+      environment,
+      port,
+      domains
+    } = req.body;
 
     // Validate inputs
     if (!name || !slug) {
       return res.status(400).json({ error: 'Name and slug are required' });
     }
 
+    if (!appType) {
+      return res.status(400).json({ error: 'Application type is required' });
+    }
+
     // Check if slug is already taken
     const existingSlug = await pool.query(
       'SELECT id FROM __sys_applications WHERE slug = $1',
@@ -57,18 +74,26 @@ router.post('/', async (req: any, res) => {
 
     // Create application
     const result = await pool.query(`
-      INSERT INTO __sys_applications (name, slug, description, created_by, repository_url, build_command, start_command, environment)
-      VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+      INSERT INTO __sys_applications (
+        name, slug, description, created_by, app_type,
+        repository_url, branch, build_command, start_command,
+        environment, port, domains
+      )
+      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
       RETURNING *
     `, [
       name,
       slug,
       description,
       userId,
+      appType || 'static',
       repositoryUrl,
+      branch || 'main',
       buildCommand,
       startCommand,
-      JSON.stringify(environment || {})
+      JSON.stringify(environment || {}),
+      port || 3000,
+      domains || []
     ]);
 
     const application = result.rows[0];
@@ -77,7 +102,7 @@ router.post('/', async (req: any, res) => {
     await pool.query(`
       INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
       VALUES ($1, 'create', 'application', $2, $3)
-    `, [userId, application.id, { name, slug }]);
+    `, [userId, application.id, { name, slug, appType }]);
 
     res.status(201).json(application);
   } catch (error) {
@@ -174,6 +199,13 @@ router.delete('/:id', async (req: any, res) => {
       return res.status(404).json({ error: 'Application not found' });
     }
 
+    // Stop container if running
+    try {
+      await deploymentService.stopApplication(id);
+    } catch (error) {
+      console.error('Error stopping application:', error);
+    }
+
     // Delete application (cascades to deployments)
     await pool.query('DELETE FROM __sys_applications WHERE id = $1', [id]);
 
@@ -190,4 +222,149 @@ router.delete('/:id', async (req: any, res) => {
   }
 });
 
+// Deploy application
+router.post('/:id/deploy', async (req: any, res) => {
+  try {
+    const userId = req.user.userId;
+    const { id } = req.params;
+
+    // Get application
+    const appResult = await pool.query(`
+      SELECT * FROM __sys_applications WHERE id = $1
+    `, [id]);
+
+    if (appResult.rows.length === 0) {
+      return res.status(404).json({ error: 'Application not found' });
+    }
+
+    const app = appResult.rows[0];
+
+    if (!app.repository_url) {
+      return res.status(400).json({ error: 'Repository URL is required for deployment' });
+    }
+
+    // Start deployment (async)
+    deploymentService.deployFromGit({
+      appId: id,
+      appType: app.app_type,
+      repositoryUrl: app.repository_url,
+      branch: app.branch || 'main',
+      buildCommand: app.build_command,
+      startCommand: app.start_command,
+      environment: app.environment,
+      port: app.port
+    }).catch(error => {
+      console.error('Deployment failed:', error);
+    });
+
+    // Log audit
+    await pool.query(`
+      INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
+      VALUES ($1, 'deploy', 'application', $2, $3)
+    `, [userId, id, { name: app.name }]);
+
+    res.json({ message: 'Deployment started', appId: id });
+  } catch (error) {
+    console.error('Error starting deployment:', error);
+    res.status(500).json({ error: 'Failed to start deployment' });
+  }
+});
+
+// Stop application
+router.post('/:id/stop', async (req: any, res) => {
+  try {
+    const userId = req.user.userId;
+    const { id } = req.params;
+
+    // Get application
+    const appResult = await pool.query(`
+      SELECT * FROM __sys_applications WHERE id = $1
+    `, [id]);
+
+    if (appResult.rows.length === 0) {
+      return res.status(404).json({ error: 'Application not found' });
+    }
+
+    const app = appResult.rows[0];
+
+    // Stop application
+    await deploymentService.stopApplication(id);
+
+    // Log audit
+    await pool.query(`
+      INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
+      VALUES ($1, 'stop', 'application', $2, $3)
+    `, [userId, id, { name: app.name }]);
+
+    res.json({ message: 'Application stopped successfully' });
+  } catch (error) {
+    console.error('Error stopping application:', error);
+    res.status(500).json({ error: 'Failed to stop application' });
+  }
+});
+
+// Get application deployments
+router.get('/:id/deployments', async (req: any, res) => {
+  try {
+    const { id } = req.params;
+    const page = parseInt(req.query.page as string) || 1;
+    const limit = parseInt(req.query.limit as string) || 20;
+    const offset = (page - 1) * limit;
+
+    // Get total count
+    const countResult = await pool.query(
+      'SELECT COUNT(*) FROM __sys_deployments WHERE application_id = $1',
+      [id]
+    );
+    const total = parseInt(countResult.rows[0].count);
+
+    // Get deployments
+    const result = await pool.query(`
+      SELECT * FROM __sys_deployments
+      WHERE application_id = $1
+      ORDER BY created_at DESC
+      LIMIT $2 OFFSET $3
+    `, [id, limit, offset]);
+
+    res.json({
+      data: result.rows,
+      total,
+      page,
+      limit,
+      totalPages: Math.ceil(total / limit),
+    });
+  } catch (error) {
+    console.error('Error listing deployments:', error);
+    res.status(500).json({ error: 'Failed to list deployments' });
+  }
+});
+
+// Get deployment logs
+router.get('/:id/deployments/:deploymentId/logs', async (req: any, res) => {
+  try {
+    const { deploymentId } = req.params;
+
+    const logs = await deploymentService.getDeploymentLogs(deploymentId);
+
+    res.json({ logs });
+  } catch (error) {
+    console.error('Error getting deployment logs:', error);
+    res.status(500).json({ error: 'Failed to get deployment logs' });
+  }
+});
+
+// Get container logs
+router.get('/:id/logs', async (req: any, res) => {
+  try {
+    const { id } = req.params;
+
+    const logs = await deploymentService.getContainerLogs(id);
+
+    res.json({ logs });
+  } catch (error) {
+    console.error('Error getting container logs:', error);
+    res.status(500).json({ error: 'Failed to get container logs' });
+  }
+});
+
 export default router;

+ 372 - 0
services/api/src/services/deployment.ts

@@ -0,0 +1,372 @@
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import Docker from 'dockerode';
+import { pool } from '../index';
+import { nginxService } from './nginx';
+
+const execAsync = promisify(exec);
+const docker = new Docker({ socketPath: '/var/run/docker.sock' });
+
+export interface DeploymentConfig {
+  appId: string;
+  appType: 'npm' | 'react' | 'nextjs' | 'php' | 'python' | 'static' | 'docker';
+  repositoryUrl: string;
+  branch: string;
+  buildCommand?: string;
+  startCommand?: string;
+  environment?: Record<string, string>;
+  port?: number;
+}
+
+export class DeploymentService {
+  private appsDir = '/apps';
+
+  /**
+   * Deploy an application from Git repository
+   */
+  async deployFromGit(config: DeploymentConfig): Promise<string> {
+    const deploymentId = await this.createDeploymentRecord(config.appId);
+
+    try {
+      // Update deployment status to building
+      await this.updateDeploymentStatus(deploymentId, 'building', 'Starting deployment...');
+
+      // Clone repository
+      const appPath = await this.cloneRepository(config);
+      await this.updateDeploymentStatus(deploymentId, 'building', 'Repository cloned successfully');
+
+      // Build Docker image based on app type
+      const imageName = await this.buildDockerImage(config, appPath);
+      await this.updateDeploymentStatus(deploymentId, 'building', `Image ${imageName} built successfully`);
+
+      // Stop existing container if any
+      await this.stopExistingContainer(config.appId);
+
+      // Start new container
+      const containerId = await this.startContainer(config, imageName);
+      await this.updateDeploymentStatus(deploymentId, 'deployed', 'Application deployed successfully');
+
+      // Update application with container ID and status
+      await this.updateApplicationStatus(config.appId, containerId, 'active');
+
+      // Update nginx configuration
+      try {
+        await nginxService.addApplication(config.appId);
+        await this.updateDeploymentStatus(deploymentId, 'deployed', 'Nginx configuration updated');
+      } catch (error: any) {
+        console.error('Failed to update nginx config:', error);
+        await this.updateDeploymentStatus(deploymentId, 'deployed', `Warning: Nginx config failed: ${error.message}`);
+      }
+
+      return deploymentId;
+    } catch (error: any) {
+      console.error('Deployment failed:', error);
+      await this.updateDeploymentStatus(deploymentId, 'failed', error.message);
+      await this.updateApplicationStatus(config.appId, null, 'error');
+      throw error;
+    }
+  }
+
+  /**
+   * Clone git repository
+   */
+  private async cloneRepository(config: DeploymentConfig): Promise<string> {
+    const appPath = path.join(this.appsDir, config.appId);
+
+    // Remove existing directory if it exists
+    try {
+      await fs.rm(appPath, { recursive: true, force: true });
+    } catch (error) {
+      // Directory doesn't exist, that's fine
+    }
+
+    // Clone repository
+    const branch = config.branch || 'main';
+    await execAsync(`git clone -b ${branch} ${config.repositoryUrl} ${appPath}`);
+
+    return appPath;
+  }
+
+  /**
+   * Build Docker image based on application type
+   */
+  private async buildDockerImage(config: DeploymentConfig, appPath: string): Promise<string> {
+    const imageName = `saas-app-${config.appId}:latest`;
+    const dockerfile = this.generateDockerfile(config);
+
+    // Write Dockerfile
+    await fs.writeFile(path.join(appPath, 'Dockerfile.generated'), dockerfile);
+
+    // Build image
+    const stream = await docker.buildImage({
+      context: appPath,
+      src: ['.']
+    }, {
+      dockerfile: 'Dockerfile.generated',
+      t: imageName
+    });
+
+    // Wait for build to complete
+    await new Promise((resolve, reject) => {
+      docker.modem.followProgress(stream, (err, res) => {
+        if (err) reject(err);
+        else resolve(res);
+      });
+    });
+
+    return imageName;
+  }
+
+  /**
+   * Generate Dockerfile based on app type
+   */
+  private generateDockerfile(config: DeploymentConfig): string {
+    const port = config.port || 3000;
+    const env = config.environment || {};
+    const envVars = Object.entries(env)
+      .map(([key, value]) => `ENV ${key}="${value}"`)
+      .join('\n');
+
+    switch (config.appType) {
+      case 'npm':
+      case 'react':
+      case 'nextjs':
+        return `
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm install ${config.appType === 'react' || config.appType === 'nextjs' ? '--production' : ''}
+
+# Copy application code
+COPY . .
+
+# Set environment variables
+${envVars}
+
+# Build if needed
+${config.buildCommand ? `RUN ${config.buildCommand}` : ''}
+
+# Expose port
+EXPOSE ${port}
+
+# Start application
+CMD ${config.startCommand ? `["sh", "-c", "${config.startCommand}"]` : '["npm", "start"]'}
+`;
+
+      case 'php':
+        return `
+FROM php:8.2-apache
+
+WORKDIR /var/www/html
+
+# Install PHP extensions
+RUN docker-php-ext-install pdo pdo_mysql mysqli
+
+# Copy application code
+COPY . .
+
+# Set environment variables
+${envVars}
+
+# Expose port
+EXPOSE 80
+
+# Apache is started automatically
+`;
+
+      case 'python':
+        return `
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Copy requirements
+COPY requirements.txt ./ 2>/dev/null || echo "Flask==2.3.0" > requirements.txt
+
+# Install dependencies
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY . .
+
+# Set environment variables
+${envVars}
+
+# Expose port
+EXPOSE ${port}
+
+# Start application
+CMD ${config.startCommand ? `["sh", "-c", "${config.startCommand}"]` : '["python", "app.py"]'}
+`;
+
+      case 'static':
+        return `
+FROM nginx:alpine
+
+# Copy static files
+COPY . /usr/share/nginx/html
+
+# Expose port
+EXPOSE 80
+`;
+
+      case 'docker':
+        // Use existing Dockerfile in repository
+        return '';
+
+      default:
+        throw new Error(`Unsupported app type: ${config.appType}`);
+    }
+  }
+
+  /**
+   * Start Docker container
+   */
+  private async startContainer(config: DeploymentConfig, imageName: string): Promise<string> {
+    const port = config.port || 3000;
+    const containerName = `saas-app-${config.appId}`;
+
+    const container = await docker.createContainer({
+      Image: imageName,
+      name: containerName,
+      Env: Object.entries(config.environment || {}).map(([key, value]) => `${key}=${value}`),
+      ExposedPorts: {
+        [`${port}/tcp`]: {}
+      },
+      HostConfig: {
+        NetworkMode: 'saas-network',
+        RestartPolicy: {
+          Name: 'unless-stopped'
+        }
+      }
+    });
+
+    await container.start();
+
+    return container.id;
+  }
+
+  /**
+   * Stop existing container for an application
+   */
+  private async stopExistingContainer(appId: string): Promise<void> {
+    const containerName = `saas-app-${appId}`;
+
+    try {
+      const container = docker.getContainer(containerName);
+      const info = await container.inspect();
+
+      if (info.State.Running) {
+        await container.stop();
+      }
+
+      await container.remove();
+    } catch (error) {
+      // Container doesn't exist, that's fine
+    }
+  }
+
+  /**
+   * Create deployment record in database
+   */
+  private async createDeploymentRecord(appId: string): Promise<string> {
+    const result = await pool.query(`
+      INSERT INTO __sys_deployments (application_id, status)
+      VALUES ($1, 'pending')
+      RETURNING id
+    `, [appId]);
+
+    return result.rows[0].id;
+  }
+
+  /**
+   * Update deployment status
+   */
+  private async updateDeploymentStatus(
+    deploymentId: string,
+    status: string,
+    log: string
+  ): Promise<void> {
+    await pool.query(`
+      UPDATE __sys_deployments
+      SET status = $1,
+          build_log = COALESCE(build_log, '') || $2 || E'\n',
+          deployed_at = CASE WHEN $1 = 'deployed' THEN CURRENT_TIMESTAMP ELSE deployed_at END
+      WHERE id = $3
+    `, [status, log, deploymentId]);
+  }
+
+  /**
+   * Update application status and container ID
+   */
+  private async updateApplicationStatus(
+    appId: string,
+    containerId: string | null,
+    status: string
+  ): Promise<void> {
+    await pool.query(`
+      UPDATE __sys_applications
+      SET status = $1, container_id = $2
+      WHERE id = $3
+    `, [status, containerId, appId]);
+  }
+
+  /**
+   * Stop and remove application
+   */
+  async stopApplication(appId: string): Promise<void> {
+    await this.stopExistingContainer(appId);
+    await this.updateApplicationStatus(appId, null, 'inactive');
+
+    // Update nginx configuration
+    try {
+      await nginxService.removeApplication(appId);
+    } catch (error: any) {
+      console.error('Failed to update nginx config:', error);
+    }
+  }
+
+  /**
+   * Get deployment logs
+   */
+  async getDeploymentLogs(deploymentId: string): Promise<string> {
+    const result = await pool.query(`
+      SELECT build_log FROM __sys_deployments WHERE id = $1
+    `, [deploymentId]);
+
+    if (result.rows.length === 0) {
+      throw new Error('Deployment not found');
+    }
+
+    return result.rows[0].build_log || '';
+  }
+
+  /**
+   * Get container logs
+   */
+  async getContainerLogs(appId: string): Promise<string> {
+    const containerName = `saas-app-${appId}`;
+
+    try {
+      const container = docker.getContainer(containerName);
+      const logs = await container.logs({
+        stdout: true,
+        stderr: true,
+        tail: 1000
+      });
+
+      return logs.toString();
+    } catch (error) {
+      throw new Error('Container not found or not running');
+    }
+  }
+}
+
+export const deploymentService = new DeploymentService();

+ 118 - 0
services/api/src/services/nginx.ts

@@ -0,0 +1,118 @@
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { pool } from '../index';
+
+const execAsync = promisify(exec);
+
+export class NginxService {
+  private nginxConfigDir = '/etc/nginx/conf.d';
+  private appsConfigFile = 'apps-proxy.conf';
+
+  /**
+   * Generate nginx configuration for all active applications
+   */
+  async generateConfiguration(): Promise<void> {
+    // Get all active applications
+    const result = await pool.query(`
+      SELECT id, slug, app_type, port, domains, status
+      FROM __sys_applications
+      WHERE status = 'active'
+    `);
+
+    const applications = result.rows;
+
+    // Generate nginx config
+    const config = this.buildNginxConfig(applications);
+
+    // Write config file
+    const configPath = path.join(this.nginxConfigDir, this.appsConfigFile);
+    await fs.writeFile(configPath, config);
+
+    // Reload nginx
+    await this.reloadNginx();
+  }
+
+  /**
+   * Build nginx configuration content
+   */
+  private buildNginxConfig(applications: any[]): string {
+    let config = '# Auto-generated application proxy configuration\n\n';
+
+    for (const app of applications) {
+      const containerName = `saas-app-${app.id}`;
+      const port = app.port || 3000;
+      const domains = app.domains || [];
+
+      // Add configuration for slug-based routing
+      config += `# Application: ${app.slug}\n`;
+      config += `location /${app.slug}/ {\n`;
+      config += `    proxy_pass http://${containerName}:${port}/;\n`;
+      config += `    proxy_http_version 1.1;\n`;
+      config += `    proxy_set_header Upgrade $http_upgrade;\n`;
+      config += `    proxy_set_header Connection 'upgrade';\n`;
+      config += `    proxy_set_header Host $host;\n`;
+      config += `    proxy_set_header X-Real-IP $remote_addr;\n`;
+      config += `    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`;
+      config += `    proxy_set_header X-Forwarded-Proto $scheme;\n`;
+      config += `    proxy_cache_bypass $http_upgrade;\n`;
+      config += `}\n\n`;
+
+      // Add server blocks for custom domains
+      for (const domain of domains) {
+        config += `# Custom domain for ${app.slug}\n`;
+        config += `server {\n`;
+        config += `    listen 80;\n`;
+        config += `    listen [::]:80;\n`;
+        config += `    server_name ${domain};\n\n`;
+        config += `    location / {\n`;
+        config += `        proxy_pass http://${containerName}:${port};\n`;
+        config += `        proxy_http_version 1.1;\n`;
+        config += `        proxy_set_header Upgrade $http_upgrade;\n`;
+        config += `        proxy_set_header Connection 'upgrade';\n`;
+        config += `        proxy_set_header Host $host;\n`;
+        config += `        proxy_set_header X-Real-IP $remote_addr;\n`;
+        config += `        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`;
+        config += `        proxy_set_header X-Forwarded-Proto $scheme;\n`;
+        config += `        proxy_cache_bypass $http_upgrade;\n`;
+        config += `    }\n`;
+        config += `}\n\n`;
+      }
+    }
+
+    return config;
+  }
+
+  /**
+   * Reload nginx configuration
+   */
+  private async reloadNginx(): Promise<void> {
+    try {
+      // Test nginx configuration first
+      await execAsync('nginx -t');
+
+      // Reload nginx
+      await execAsync('nginx -s reload');
+    } catch (error: any) {
+      console.error('Failed to reload nginx:', error.message);
+      throw new Error(`Failed to reload nginx: ${error.message}`);
+    }
+  }
+
+  /**
+   * Add application to nginx configuration
+   */
+  async addApplication(appId: string): Promise<void> {
+    await this.generateConfiguration();
+  }
+
+  /**
+   * Remove application from nginx configuration
+   */
+  async removeApplication(appId: string): Promise<void> {
+    await this.generateConfiguration();
+  }
+}
+
+export const nginxService = new NginxService();