1+ #!/usr/bin/env node
2+
3+ import { Command } from 'commander'
4+ import chalk from 'chalk'
5+ import { spawn , execSync } from 'child_process'
6+ import { mkdirSync , existsSync } from 'fs'
7+ import { join } from 'path'
8+ import { createInterface } from 'readline'
9+ import { homedir } from 'os'
10+ import inquirer from 'inquirer'
11+
12+ const NETWORK_NAME = 'simstudio-network'
13+ const DB_CONTAINER = 'simstudio-db'
14+ const MIGRATIONS_CONTAINER = 'simstudio-migrations'
15+ const APP_CONTAINER = 'simstudio-app'
16+ const DEFAULT_PORT = '3000'
17+
18+ const program = new Command ( )
19+
20+ program
21+ . name ( 'simstudio' )
22+ . description ( 'Run Sim Studio using Docker' )
23+ . version ( '0.1.0' )
24+
25+ program
26+ . option ( '-p, --port <port>' , 'Port to run Sim Studio on' , DEFAULT_PORT )
27+ . option ( '-y, --yes' , 'Skip interactive prompts and use defaults' )
28+ . option ( '--no-pull' , 'Skip pulling the latest Docker images' )
29+
30+ function isDockerRunning ( ) : Promise < boolean > {
31+ return new Promise ( ( resolve ) => {
32+ const docker = spawn ( 'docker' , [ 'info' ] )
33+
34+ docker . on ( 'close' , ( code ) => {
35+ resolve ( code === 0 )
36+ } )
37+ } )
38+ }
39+
40+ async function runCommand ( command : string [ ] ) : Promise < boolean > {
41+ return new Promise ( ( resolve ) => {
42+ const process = spawn ( command [ 0 ] , command . slice ( 1 ) , { stdio : 'inherit' } )
43+ process . on ( 'error' , ( ) => {
44+ resolve ( false )
45+ } )
46+ process . on ( 'close' , ( code ) => {
47+ resolve ( code === 0 )
48+ } )
49+ } )
50+ }
51+
52+ async function ensureNetworkExists ( ) : Promise < boolean > {
53+ try {
54+ const networks = execSync ( 'docker network ls --format "{{.Name}}"' ) . toString ( )
55+ if ( ! networks . includes ( NETWORK_NAME ) ) {
56+ console . log ( chalk . blue ( `🔄 Creating Docker network '${ NETWORK_NAME } '...` ) )
57+ return await runCommand ( [ 'docker' , 'network' , 'create' , NETWORK_NAME ] )
58+ }
59+ return true
60+ } catch ( error ) {
61+ console . error ( 'Failed to check networks:' , error )
62+ return false
63+ }
64+ }
65+
66+ async function pullImage ( image : string ) : Promise < boolean > {
67+ console. log ( chalk . blue ( `🔄 Pulling image ${ image } ...` ) )
68+ return await runCommand ( [ 'docker' , 'pull' , image ] )
69+ }
70+
71+ async function stopAndRemoveContainer ( name : string ) : Promise < void > {
72+ try {
73+ execSync ( `docker stop ${ name } 2>/dev/null || true` )
74+ execSync ( `docker rm ${ name } 2>/dev/null || true` )
75+ } catch ( error ) {
76+ // Ignore errors, container might not exist
77+ }
78+ }
79+
80+ async function cleanupExistingContainers ( ) : Promise < void > {
81+ console . log ( chalk . blue ( '🧹 Cleaning up any existing containers...' ) )
82+ await stopAndRemoveContainer ( APP_CONTAINER )
83+ await stopAndRemoveContainer ( DB_CONTAINER )
84+ await stopAndRemoveContainer ( MIGRATIONS_CONTAINER )
85+ }
86+
87+ async function main ( ) {
88+ const options = program . parse ( ) . opts ( )
89+
90+ console . log ( chalk . blue ( '🚀 Starting Sim Studio...' ) )
91+
92+ // Check if Docker is installed and running
93+ const dockerRunning = await isDockerRunning ( )
94+ if ( ! dockerRunning ) {
95+ console . error ( chalk . red ( '❌ Docker is not running or not installed. Please start Docker and try again.' ) )
96+ process . exit ( 1 )
97+ }
98+
99+ // Use port from options, with 3000 as default
100+ const port = options . port
101+
102+ // Pull latest images if not skipped
103+ if ( options . pull ) {
104+ await pullImage ( 'ghcr.io/simstudioai/simstudio:latest' )
105+ await pullImage ( 'ghcr.io/simstudioai/migrations:latest' )
106+ await pullImage ( 'postgres:17-alpine' )
107+ }
108+
109+ // Ensure Docker network exists
110+ if ( ! await ensureNetworkExists ( ) ) {
111+ console . error ( chalk . red ( '❌ Failed to create Docker network' ) )
112+ process . exit ( 1 )
113+ }
114+
115+ // Clean up any existing containers
116+ await cleanupExistingContainers ( )
117+
118+ // Create data directory
119+ const dataDir = join ( homedir ( ) , '.simstudio' , 'data' )
120+ if ( ! existsSync ( dataDir ) ) {
121+ try {
122+ mkdirSync ( dataDir , { recursive : true } )
123+ } catch ( error ) {
124+ console . error ( chalk . red ( `❌ Failed to create data directory: ${ dataDir } ` ) )
125+ process . exit ( 1 )
126+ }
127+ }
128+
129+ // Start PostgreSQL container
130+ console . log ( chalk . blue ( '🔄 Starting PostgreSQL database...' ) )
131+ const dbSuccess = await runCommand ( [
132+ 'docker' , 'run' , '-d' ,
133+ '--name' , DB_CONTAINER ,
134+ '--network' , NETWORK_NAME ,
135+ '-e' , 'POSTGRES_USER=postgres' ,
136+ '-e' , 'POSTGRES_PASSWORD=postgres' ,
137+ '-e' , 'POSTGRES_DB=simstudio' ,
138+ '-v' , `${ dataDir } /postgres:/var/lib/postgresql/data` ,
139+ '-p' , '5432:5432' ,
140+ 'postgres:17-alpine'
141+ ] )
142+
143+ if ( ! dbSuccess ) {
144+ console . error ( chalk . red ( '❌ Failed to start PostgreSQL' ) )
145+ process . exit ( 1 )
146+ }
147+
148+ // Wait for PostgreSQL to be ready
149+ console . log ( chalk . blue ( '⏳ Waiting for PostgreSQL to be ready...' ) )
150+ let pgReady = false
151+ for ( let i = 0 ; i < 30 ; i ++ ) {
152+ try {
153+ execSync ( `docker exec ${ DB_CONTAINER } pg_isready -U postgres` )
154+ pgReady = true
155+ break
156+ } catch ( error ) {
157+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) )
158+ }
159+ }
160+
161+ if ( ! pgReady ) {
162+ console . error ( chalk . red ( '❌ PostgreSQL failed to become ready' ) )
163+ process . exit ( 1 )
164+ }
165+
166+ // Run migrations
167+ console . log ( chalk . blue ( '🔄 Running database migrations...' ) )
168+ const migrationsSuccess = await runCommand ( [
169+ 'docker' , 'run' , '--rm' ,
170+ '--name' , MIGRATIONS_CONTAINER ,
171+ '--network' , NETWORK_NAME ,
172+ '-e' , `DATABASE_URL=postgresql://postgres:postgres@${ DB_CONTAINER } :5432/simstudio` ,
173+ 'ghcr.io/simstudioai/migrations:latest' ,
174+ 'bun' , 'run' , 'db:push'
175+ ] )
176+
177+ if ( ! migrationsSuccess ) {
178+ console . error ( chalk . red ( '❌ Failed to run migrations' ) )
179+ process . exit ( 1 )
180+ }
181+
182+ // Start the main application
183+ console . log ( chalk . blue ( '🔄 Starting Sim Studio...' ) )
184+ const appSuccess = await runCommand ( [
185+ 'docker' , 'run' , '-d' ,
186+ '--name' , APP_CONTAINER ,
187+ '--network' , NETWORK_NAME ,
188+ '-p' , `${ port } :3000` ,
189+ '-e' , `DATABASE_URL=postgresql://postgres:postgres@${ DB_CONTAINER } :5432/simstudio` ,
190+ '-e' , `BETTER_AUTH_URL=http://localhost:${ port } ` ,
191+ '-e' , `NEXT_PUBLIC_APP_URL=http://localhost:${ port } ` ,
192+ '-e' , 'BETTER_AUTH_SECRET=your_auth_secret_here' ,
193+ '-e' , 'ENCRYPTION_KEY=your_encryption_key_here' ,
194+ 'ghcr.io/simstudioai/simstudio:latest'
195+ ] )
196+
197+ if ( ! appSuccess ) {
198+ console . error ( chalk . red ( '❌ Failed to start Sim Studio' ) )
199+ process . exit ( 1 )
200+ }
201+
202+ console . log ( chalk . green ( `✅ Sim Studio is now running at ${ chalk . bold ( `http://localhost:${ port } ` ) } ` ) )
203+ console . log ( chalk . yellow ( `🛑 To stop all containers, run: ${ chalk . bold ( 'docker stop simstudio-app simstudio-db' ) } ` ) )
204+
205+ // Handle Ctrl+C
206+ const rl = createInterface ( {
207+ input : process . stdin ,
208+ output : process . stdout
209+ } )
210+
211+ rl . on ( 'SIGINT' , async ( ) => {
212+ console . log ( chalk . yellow ( '\n🛑 Stopping Sim Studio...' ) )
213+
214+ // Stop containers
215+ await stopAndRemoveContainer ( APP_CONTAINER )
216+ await stopAndRemoveContainer ( DB_CONTAINER )
217+
218+ console . log ( chalk . green ( '✅ Sim Studio has been stopped' ) )
219+ process . exit ( 0 )
220+ } )
221+ }
222+
223+ main ( ) . catch ( error => {
224+ console . error ( chalk . red ( '❌ An error occurred:' ) , error )
225+ process . exit ( 1 )
226+ } )
0 commit comments