Skip to content
Merged
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
58 changes: 58 additions & 0 deletions .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Publish CLI Package

on:
push:
branches: [main]
paths:
- 'packages/simstudio/**'

jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org/'

- name: Install dependencies
working-directory: packages/simstudio
run: bun install

- name: Build package
working-directory: packages/simstudio
run: bun run build

- name: Get package version
id: package_version
working-directory: packages/simstudio
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

- name: Check if version already exists
id: version_check
run: |
if npm view simstudio@${{ steps.package_version.outputs.version }} version &> /dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Publish to npm
if: steps.version_check.outputs.exists == 'false'
working-directory: packages/simstudio
run: npm publish --access=public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Log skipped publish
if: steps.version_check.outputs.exists == 'true'
run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on npm"
259 changes: 205 additions & 54 deletions bun.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
{
"devDependencies": {
"@next/env": "^15.3.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"dotenv-cli": "^8.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"turbo": "2.5.3"
},
"packageManager": "bun@1.2.12",
"name": "simstudio",
"packageManager": "bun@1.2.12",
"version": "0.0.0",
"private": true,
"license": "Apache-2.0",
Expand All @@ -32,5 +24,13 @@
"dependencies": {
"@t3-oss/env-nextjs": "0.13.4",
"@vercel/analytics": "1.5.0"
},
"devDependencies": {
"@next/env": "^15.3.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"dotenv-cli": "^8.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"turbo": "2.5.3"
}
}
}
52 changes: 52 additions & 0 deletions packages/simstudio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "simstudio",
"version": "0.1.16",
"description": "Sim Studio CLI - Run Sim Studio with a single command",
"main": "dist/index.js",
"bin": {
"simstudio": "dist/index.js"
},
"scripts": {
"build": "bun run build:tsc",
"build:tsc": "tsc",
"prepublishOnly": "bun run build"
},
"files": [ "dist" ],
"keywords": [
"simstudio",
"ai",
"workflow",
"ui",
"cli",
"sim",
"sim-studio",
"agent",
"agents",
"automation",
"docker"
],
"author": "Sim Studio",
"license": "Apache-2.0",
"dependencies": {
"chalk": "^4.1.2",
"commander": "^11.1.0",
"dotenv": "^16.3.1",
"inquirer": "^8.2.6",
"listr2": "^6.6.1"
},
"devDependencies": {
"@types/inquirer": "^8.2.6",
"@types/node": "^20.5.1",
"typescript": "^5.1.6"
},
"engines": {
"node": ">=16"
},
"turbo": {
"tasks": {
"build": {
"outputs": ["dist/**"]
}
}
}
}
226 changes: 226 additions & 0 deletions packages/simstudio/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env node

import { Command } from 'commander'
import chalk from 'chalk'
import { spawn, execSync } from 'child_process'
import { mkdirSync, existsSync } from 'fs'
import { join } from 'path'
import { createInterface } from 'readline'
import { homedir } from 'os'
import inquirer from 'inquirer'

const NETWORK_NAME = 'simstudio-network'
const DB_CONTAINER = 'simstudio-db'
const MIGRATIONS_CONTAINER = 'simstudio-migrations'
const APP_CONTAINER = 'simstudio-app'
const DEFAULT_PORT = '3000'

const program = new Command()

program
.name('simstudio')
.description('Run Sim Studio using Docker')
.version('0.1.0')

program
.option('-p, --port <port>', 'Port to run Sim Studio on', DEFAULT_PORT)
.option('-y, --yes', 'Skip interactive prompts and use defaults')
.option('--no-pull', 'Skip pulling the latest Docker images')

function isDockerRunning(): Promise<boolean> {
return new Promise((resolve) => {
const docker = spawn('docker', ['info'])

docker.on('close', (code) => {
resolve(code === 0)
})
})
}

async function runCommand(command: string[]): Promise<boolean> {
return new Promise((resolve) => {
const process = spawn(command[0], command.slice(1), { stdio: 'inherit' })
process.on('error', () => {
resolve(false)
})
process.on('close', (code) => {
resolve(code === 0)
})
})
}

async function ensureNetworkExists(): Promise<boolean> {
try {
const networks = execSync('docker network ls --format "{{.Name}}"').toString()
if (!networks.includes(NETWORK_NAME)) {
console.log(chalk.blue(`🔄 Creating Docker network '${NETWORK_NAME}'...`))
return await runCommand(['docker', 'network', 'create', NETWORK_NAME])
}
return true
} catch (error) {
console.error('Failed to check networks:', error)
return false
}
}

async function pullImage(image: string): Promise<boolean> {
console.log(chalk.blue(`🔄 Pulling image ${image}...`))
return await runCommand(['docker', 'pull', image])
}

async function stopAndRemoveContainer(name: string): Promise<void> {
try {
execSync(`docker stop ${name} 2>/dev/null || true`)
execSync(`docker rm ${name} 2>/dev/null || true`)
} catch (error) {
// Ignore errors, container might not exist
}
}

async function cleanupExistingContainers(): Promise<void> {
console.log(chalk.blue('🧹 Cleaning up any existing containers...'))
await stopAndRemoveContainer(APP_CONTAINER)
await stopAndRemoveContainer(DB_CONTAINER)
await stopAndRemoveContainer(MIGRATIONS_CONTAINER)
}

async function main() {
const options = program.parse().opts()

console.log(chalk.blue('🚀 Starting Sim Studio...'))

// Check if Docker is installed and running
const dockerRunning = await isDockerRunning()
if (!dockerRunning) {
console.error(chalk.red('❌ Docker is not running or not installed. Please start Docker and try again.'))
process.exit(1)
}

// Use port from options, with 3000 as default
const port = options.port

// Pull latest images if not skipped
if (options.pull) {
await pullImage('ghcr.io/simstudioai/simstudio:latest')
await pullImage('ghcr.io/simstudioai/migrations:latest')
await pullImage('postgres:17-alpine')
}

// Ensure Docker network exists
if (!await ensureNetworkExists()) {
console.error(chalk.red('❌ Failed to create Docker network'))
process.exit(1)
}

// Clean up any existing containers
await cleanupExistingContainers()

// Create data directory
const dataDir = join(homedir(), '.simstudio', 'data')
if (!existsSync(dataDir)) {
try {
mkdirSync(dataDir, { recursive: true })
} catch (error) {
console.error(chalk.red(`❌ Failed to create data directory: ${dataDir}`))
process.exit(1)
}
}

// Start PostgreSQL container
console.log(chalk.blue('🔄 Starting PostgreSQL database...'))
const dbSuccess = await runCommand([
'docker', 'run', '-d',
'--name', DB_CONTAINER,
'--network', NETWORK_NAME,
'-e', 'POSTGRES_USER=postgres',
'-e', 'POSTGRES_PASSWORD=postgres',
'-e', 'POSTGRES_DB=simstudio',
'-v', `${dataDir}/postgres:/var/lib/postgresql/data`,
'-p', '5432:5432',
'postgres:17-alpine'
])

if (!dbSuccess) {
console.error(chalk.red('❌ Failed to start PostgreSQL'))
process.exit(1)
}

// Wait for PostgreSQL to be ready
console.log(chalk.blue('⏳ Waiting for PostgreSQL to be ready...'))
let pgReady = false
for (let i = 0; i < 30; i++) {
try {
execSync(`docker exec ${DB_CONTAINER} pg_isready -U postgres`)
pgReady = true
break
} catch (error) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}

if (!pgReady) {
console.error(chalk.red('❌ PostgreSQL failed to become ready'))
process.exit(1)
}

// Run migrations
console.log(chalk.blue('🔄 Running database migrations...'))
const migrationsSuccess = await runCommand([
'docker', 'run', '--rm',
'--name', MIGRATIONS_CONTAINER,
'--network', NETWORK_NAME,
'-e', `DATABASE_URL=postgresql://postgres:postgres@${DB_CONTAINER}:5432/simstudio`,
'ghcr.io/simstudioai/migrations:latest',
'bun', 'run', 'db:push'
])

if (!migrationsSuccess) {
console.error(chalk.red('❌ Failed to run migrations'))
process.exit(1)
}

// Start the main application
console.log(chalk.blue('🔄 Starting Sim Studio...'))
const appSuccess = await runCommand([
'docker', 'run', '-d',
'--name', APP_CONTAINER,
'--network', NETWORK_NAME,
'-p', `${port}:3000`,
'-e', `DATABASE_URL=postgresql://postgres:postgres@${DB_CONTAINER}:5432/simstudio`,
'-e', `BETTER_AUTH_URL=http://localhost:${port}`,
'-e', `NEXT_PUBLIC_APP_URL=http://localhost:${port}`,
'-e', 'BETTER_AUTH_SECRET=your_auth_secret_here',
'-e', 'ENCRYPTION_KEY=your_encryption_key_here',
'ghcr.io/simstudioai/simstudio:latest'
])

if (!appSuccess) {
console.error(chalk.red('❌ Failed to start Sim Studio'))
process.exit(1)
}

console.log(chalk.green(`✅ Sim Studio is now running at ${chalk.bold(`http://localhost:${port}`)}`))
console.log(chalk.yellow(`🛑 To stop all containers, run: ${chalk.bold('docker stop simstudio-app simstudio-db')}`))

// Handle Ctrl+C
const rl = createInterface({
input: process.stdin,
output: process.stdout
})

rl.on('SIGINT', async () => {
console.log(chalk.yellow('\n🛑 Stopping Sim Studio...'))

// Stop containers
await stopAndRemoveContainer(APP_CONTAINER)
await stopAndRemoveContainer(DB_CONTAINER)

console.log(chalk.green('✅ Sim Studio has been stopped'))
process.exit(0)
})
}

main().catch(error => {
console.error(chalk.red('❌ An error occurred:'), error)
process.exit(1)
})
16 changes: 16 additions & 0 deletions packages/simstudio/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading