Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions apps/sim/app/api/workflows/[id]/export-service/generate-zip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/**
* ZIP generation utilities for export service.
*/
import { readFileSync, readdirSync, statSync } from 'fs'
import { join } from 'path'
import JSZip from 'jszip'

/**
* Read all template files from the templates directory.
*/
function loadTemplates(): Record<string, string> {
const templatesDir = join(__dirname, 'templates')
const templates: Record<string, string> = {}

function readDir(dir: string, prefix: string = '') {
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = join(dir, entry)
const relativePath = prefix ? `${prefix}/${entry}` : entry
const stat = statSync(fullPath)

if (stat.isDirectory()) {
readDir(fullPath, relativePath)
} else {
templates[relativePath] = readFileSync(fullPath, 'utf-8')
}
}
}

readDir(templatesDir)
return templates
}

// Load templates once at module initialization
let TEMPLATES: Record<string, string> | null = null

function getTemplates(): Record<string, string> {
if (!TEMPLATES) {
TEMPLATES = loadTemplates()
}
return TEMPLATES
}

export interface WorkflowVariable {
id: string
name: string
type: string
value: unknown
}

export interface GenerateZipOptions {
workflowName: string
workflowState: Record<string, unknown>
decryptedEnv: Record<string, string>
workflowVariables: WorkflowVariable[]
}

/**
* Build the .env file content.
*/
function buildEnvContent(
workflowName: string,
decryptedEnv: Record<string, string>,
workflowVariables: WorkflowVariable[]
): string {
const lines = [
`# ${workflowName} - Environment Variables`,
'# Auto-generated with decrypted values',
'',
'# API Keys',
]

// Add API keys from environment
const apiKeyPatterns = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY']
for (const key of apiKeyPatterns) {
if (decryptedEnv[key]) {
lines.push(`${key}=${decryptedEnv[key]}`)
}
}

// Add any other environment variables
for (const [key, value] of Object.entries(decryptedEnv)) {
if (!apiKeyPatterns.includes(key)) {
lines.push(`${key}=${value}`)
}
}

// Add workflow variables
lines.push('')
lines.push('# Workflow Variables (initial values)')
for (const variable of workflowVariables) {
const value =
typeof variable.value === 'object' ? JSON.stringify(variable.value) : variable.value
lines.push(`WORKFLOW_VAR_${variable.name}=${value}`)
}

lines.push('')
lines.push('# Server Configuration')
lines.push('# HOST=0.0.0.0')
lines.push('# PORT=8080')
lines.push('# WORKFLOW_PATH=workflow.json')
lines.push('')

return lines.join('\n')
}

/**
* Build the .env.example file content (masked API keys).
*/
function buildEnvExampleContent(envContent: string): string {
return envContent
.split('\n')
.map((line) => {
if (line.includes('=') && !line.startsWith('#') && !line.startsWith('WORKFLOW_VAR_')) {
const [key] = line.split('=')
return `${key}=your-key-here`
}
return line
})
.join('\n')
}

/**
* Build the README.md content.
*/
function buildReadmeContent(workflowName: string, serviceName: string): string {
return `# ${workflowName}

Standalone workflow service exported from Sim Studio.

## Quick Start

\`\`\`bash
# Install dependencies
pip install -r requirements.txt

# Start server
uvicorn main:app --port 8080

# Execute workflow
curl -X POST http://localhost:8080/execute \\
-H "Content-Type: application/json" \\
-d '{"your": "input"}'
\`\`\`

## Docker Deployment

\`\`\`bash
# Build and run with Docker Compose
docker compose up -d

# Or build manually
docker build -t ${serviceName} .
docker run -p 8080:8080 --env-file .env ${serviceName}
\`\`\`

## Files

- \`workflow.json\` - Workflow definition
- \`.env\` - Environment variables (API keys included)
- \`.env.example\` - Template without sensitive values
- \`main.py\` - FastAPI server
- \`executor.py\` - DAG execution engine
- \`handlers/\` - Block type handlers
- \`Dockerfile\` - Container configuration
- \`docker-compose.yml\` - Docker Compose setup

## API

- \`GET /health\` - Health check
- \`POST /execute\` - Execute workflow with input

## Security Notice

⚠️ **IMPORTANT**: The \`.env\` file contains sensitive API keys.

- **Never commit \`.env\` to version control** - add it to \`.gitignore\`
- Use \`.env.example\` as a template for team members
- In production, use secure environment variable management (e.g., AWS Secrets Manager, Docker secrets, Kubernetes secrets)
- Consider using environment-specific configurations for different deployments

## MCP Tool Support

This service supports MCP (Model Context Protocol) tools via the official Python SDK.
MCP servers must be running and accessible at their configured URLs for tool execution to work.

Exported at: ${new Date().toISOString()}
`
}

/**
* Generate the service ZIP file.
*/
export async function generateServiceZip(options: GenerateZipOptions): Promise<Buffer> {
const { workflowName, workflowState, decryptedEnv, workflowVariables } = options

const templates = getTemplates()
const zip = new JSZip()
const serviceName = workflowName.replace(/[^a-z0-9]/gi, '-').toLowerCase()
const folder = zip.folder(serviceName)!

// Add workflow.json
folder.file('workflow.json', JSON.stringify(workflowState, null, 2))

// Add .env
const envContent = buildEnvContent(workflowName, decryptedEnv, workflowVariables)
folder.file('.env', envContent)

// Add .env.example (masked)
folder.file('.env.example', buildEnvExampleContent(envContent))

// Add all template files
for (const [filename, content] of Object.entries(templates)) {
folder.file(filename, content)
}

// Add README.md
folder.file('README.md', buildReadmeContent(workflowName, serviceName))

// Generate ZIP buffer
return zip.generateAsync({ type: 'nodebuffer' }) as Promise<Buffer>
}

/**
* Get the service name from workflow name.
*/
export function getServiceName(workflowName: string): string {
return workflowName.replace(/[^a-z0-9]/gi, '-').toLowerCase()
}
Loading