Skip to content

Commit f2894e6

Browse files
authored
feat(package): added npm package to run sim studio (#376)
* added npm pkg for sim studio * added cache scope for new pkg * added optional port config, default to 3000 * remove accidental package lock * updated package version * added keywords * acknowledged PR comments
1 parent 18ce266 commit f2894e6

File tree

7 files changed

+568
-65
lines changed

7 files changed

+568
-65
lines changed

.github/workflows/publish-cli.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Publish CLI Package
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'packages/simstudio/**'
8+
9+
jobs:
10+
publish-npm:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Bun
17+
uses: oven-sh/setup-bun@v1
18+
with:
19+
bun-version: latest
20+
21+
- name: Setup Node.js for npm publishing
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: '18'
25+
registry-url: 'https://registry.npmjs.org/'
26+
27+
- name: Install dependencies
28+
working-directory: packages/simstudio
29+
run: bun install
30+
31+
- name: Build package
32+
working-directory: packages/simstudio
33+
run: bun run build
34+
35+
- name: Get package version
36+
id: package_version
37+
working-directory: packages/simstudio
38+
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
39+
40+
- name: Check if version already exists
41+
id: version_check
42+
run: |
43+
if npm view simstudio@${{ steps.package_version.outputs.version }} version &> /dev/null; then
44+
echo "exists=true" >> $GITHUB_OUTPUT
45+
else
46+
echo "exists=false" >> $GITHUB_OUTPUT
47+
fi
48+
49+
- name: Publish to npm
50+
if: steps.version_check.outputs.exists == 'false'
51+
working-directory: packages/simstudio
52+
run: npm publish --access=public
53+
env:
54+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
55+
56+
- name: Log skipped publish
57+
if: steps.version_check.outputs.exists == 'true'
58+
run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on npm"

bun.lock

Lines changed: 205 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
{
2-
"devDependencies": {
3-
"@next/env": "^15.3.2",
4-
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
5-
"dotenv-cli": "^8.0.0",
6-
"prettier": "^3.5.3",
7-
"prettier-plugin-tailwindcss": "^0.6.11",
8-
"turbo": "2.5.3"
9-
},
10-
"packageManager": "bun@1.2.12",
112
"name": "simstudio",
3+
"packageManager": "bun@1.2.12",
124
"version": "0.0.0",
135
"private": true,
146
"license": "Apache-2.0",
@@ -33,5 +25,13 @@
3325
"dependencies": {
3426
"@t3-oss/env-nextjs": "0.13.4",
3527
"@vercel/analytics": "1.5.0"
28+
},
29+
"devDependencies": {
30+
"@next/env": "^15.3.2",
31+
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
32+
"dotenv-cli": "^8.0.0",
33+
"prettier": "^3.5.3",
34+
"prettier-plugin-tailwindcss": "^0.6.11",
35+
"turbo": "2.5.3"
3636
}
37-
}
37+
}

packages/simstudio/package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "simstudio",
3+
"version": "0.1.16",
4+
"description": "Sim Studio CLI - Run Sim Studio with a single command",
5+
"main": "dist/index.js",
6+
"bin": {
7+
"simstudio": "dist/index.js"
8+
},
9+
"scripts": {
10+
"build": "bun run build:tsc",
11+
"build:tsc": "tsc",
12+
"prepublishOnly": "bun run build"
13+
},
14+
"files": [ "dist" ],
15+
"keywords": [
16+
"simstudio",
17+
"ai",
18+
"workflow",
19+
"ui",
20+
"cli",
21+
"sim",
22+
"sim-studio",
23+
"agent",
24+
"agents",
25+
"automation",
26+
"docker"
27+
],
28+
"author": "Sim Studio",
29+
"license": "Apache-2.0",
30+
"dependencies": {
31+
"chalk": "^4.1.2",
32+
"commander": "^11.1.0",
33+
"dotenv": "^16.3.1",
34+
"inquirer": "^8.2.6",
35+
"listr2": "^6.6.1"
36+
},
37+
"devDependencies": {
38+
"@types/inquirer": "^8.2.6",
39+
"@types/node": "^20.5.1",
40+
"typescript": "^5.1.6"
41+
},
42+
"engines": {
43+
"node": ">=16"
44+
},
45+
"turbo": {
46+
"tasks": {
47+
"build": {
48+
"outputs": ["dist/**"]
49+
}
50+
}
51+
}
52+
}

packages/simstudio/src/index.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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+
})

packages/simstudio/tsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2020",
4+
"module": "ESNext",
5+
"moduleResolution": "node",
6+
"outDir": "./dist",
7+
"rootDir": "./src",
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"declaration": true,
12+
"forceConsistentCasingInFileNames": true
13+
},
14+
"include": ["src/**/*"],
15+
"exclude": ["node_modules", "dist"]
16+
}

0 commit comments

Comments
 (0)