Files
sim/packages/@sim/cli/src/commands/start.ts
T

280 lines
8.8 KiB
TypeScript

import chalk from 'chalk'
import { spawn } from 'child_process'
import { execSync } from 'child_process'
import fs from 'fs'
import { createWriteStream } from 'fs'
import https from 'https'
import os from 'os'
import path from 'path'
import { extract } from 'tar'
import { config } from '../utils/config'
import { SimpleSpinner, createSpinner } from '../utils/spinner'
interface StartOptions {
port: string
debug: boolean
}
// Constants for standalone app
const SIM_HOME_DIR = path.join(os.homedir(), '.sim-studio')
const SIM_STANDALONE_DIR = path.join(SIM_HOME_DIR, 'standalone')
const SIM_VERSION_FILE = path.join(SIM_HOME_DIR, 'version.json')
const DOWNLOAD_URL =
'https://github.com/simstudioai/sim/releases/download/v0.1.0/sim-standalone.tar.gz'
const STANDALONE_VERSION = '0.1.0'
/**
* Start command that launches Sim Studio using local storage
*/
export async function start(options: StartOptions) {
// Update config with provided options
config.set('port', options.port)
config.set('debug', options.debug)
config.set('lastRun', new Date().toISOString())
const port = options.port || '3000'
const debug = options.debug || false
// Show starting message
const spinner = createSpinner(`Starting Sim Studio on port ${port}...`).start()
try {
// Set environment variables for using local storage
const env = {
...process.env,
PORT: port,
USE_LOCAL_STORAGE: 'true', // Key environment variable to switch to local storage
NODE_ENV: debug ? 'development' : ('production' as const),
DEBUG: debug ? '*' : undefined,
}
// Try to find the main package.json to determine if we're running from within the repo
// or as an installed npm package
const isInProjectDirectory = checkIfInProjectDirectory()
let simProcess
if (isInProjectDirectory) {
// Running from within the project directory - we'll use the existing
// Next.js setup directly
spinner.text = 'Detected Sim Studio project, starting with local configuration...'
simProcess = spawn('npm', ['run', 'dev'], {
env: env as NodeJS.ProcessEnv,
stdio: 'inherit',
shell: true,
})
} else {
// Running from outside the project via npx - we'll download and start a standalone version
spinner.text = 'Setting up standalone Sim Studio...'
// Create the .sim-studio directory if it doesn't exist
if (!fs.existsSync(SIM_HOME_DIR)) {
fs.mkdirSync(SIM_HOME_DIR, { recursive: true })
}
// Check if we already have the standalone version
let needsDownload = true
if (fs.existsSync(SIM_VERSION_FILE)) {
try {
const versionInfo = JSON.parse(fs.readFileSync(SIM_VERSION_FILE, 'utf8'))
if (versionInfo.version === STANDALONE_VERSION) {
needsDownload = false
}
} catch (error) {
// If there's an error reading the version file, download again
needsDownload = true
}
}
// Download and extract if needed
if (needsDownload) {
try {
await downloadStandaloneApp(spinner)
} catch (error) {
spinner.fail(
`Failed to download Sim Studio: ${error instanceof Error ? error.message : String(error)}`
)
console.log(`\n${chalk.yellow('⚠️')} If you're having network issues, you can try:
1. Check your internet connection
2. Try again later
3. Run Sim Studio directly from a cloned repository`)
process.exit(1)
}
} else {
spinner.text = 'Using cached Sim Studio standalone version...'
}
// Start the standalone app
spinner.text = 'Starting Sim Studio standalone...'
// Make sure the standalone directory exists
if (
!fs.existsSync(SIM_STANDALONE_DIR) ||
!fs.existsSync(path.join(SIM_STANDALONE_DIR, 'server.js'))
) {
spinner.fail('Standalone app files are missing. Re-run to download again.')
// Force a fresh download next time
if (fs.existsSync(SIM_VERSION_FILE)) {
fs.unlinkSync(SIM_VERSION_FILE)
}
process.exit(1)
}
// Start the standalone Node.js server
const standaloneEnv = {
...env,
SIM_STUDIO_PORT: port,
}
simProcess = spawn('node', ['server.js'], {
cwd: SIM_STANDALONE_DIR,
env: standaloneEnv as NodeJS.ProcessEnv,
stdio: 'inherit',
shell: true,
})
}
// Successful start
spinner.succeed(`Sim Studio is running on ${chalk.cyan(`http://localhost:${port}`)}`)
console.log(`
${chalk.green('✓')} Using local storage mode - your data will be stored in the browser
${chalk.green('✓')} Any changes will be persisted between sessions through localStorage
${chalk.yellow('i')} Press ${chalk.bold('Ctrl+C')} to stop the server
`)
// Handle process termination
process.on('SIGINT', () => {
console.log(`\n${chalk.yellow('⚠️')} Shutting down Sim Studio...`)
simProcess.kill('SIGINT')
process.exit(0)
})
// Return the process for testing purposes
return simProcess
} catch (error) {
spinner.fail('Failed to start Sim Studio')
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error)
process.exit(1)
}
}
/**
* Checks if we're running in a Sim Studio project directory
*/
function checkIfInProjectDirectory(): boolean {
// Check if we have package.json that looks like a Sim Studio project
try {
const packageJsonPath = path.join(process.cwd(), 'package.json')
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
// Check if it looks like our project
if (
packageJson.name === 'sim' ||
packageJson.name === 'sim-studio' ||
(packageJson.dependencies &&
(packageJson.dependencies['next'] || packageJson.dependencies['@sim/cli']))
) {
return true
}
}
// Also check for Next.js app files
const nextConfigPath = path.join(process.cwd(), 'next.config.js')
const nextTsConfigPath = path.join(process.cwd(), 'next.config.ts')
if (fs.existsSync(nextConfigPath) || fs.existsSync(nextTsConfigPath)) {
return true
}
} catch (error) {
// If we can't read/parse package.json, assume we're not in a project directory
}
return false
}
/**
* Downloads and extracts the standalone app
*/
async function downloadStandaloneApp(spinner: SimpleSpinner): Promise<void> {
return new Promise((resolve, reject) => {
// Create temp directory
const tmpDir = path.join(os.tmpdir(), `sim-download-${Date.now()}`)
fs.mkdirSync(tmpDir, { recursive: true })
const tarballPath = path.join(tmpDir, 'sim-standalone.tar.gz')
const file = createWriteStream(tarballPath)
spinner.text = 'Downloading Sim Studio...'
// Download the tarball
https
.get(DOWNLOAD_URL, (response) => {
if (response.statusCode !== 200) {
spinner.fail(`Failed to download: ${response.statusCode}`)
return reject(new Error(`Download failed with status code: ${response.statusCode}`))
}
response.pipe(file)
file.on('finish', () => {
file.close()
// Clear the standalone directory if it exists
if (fs.existsSync(SIM_STANDALONE_DIR)) {
fs.rmSync(SIM_STANDALONE_DIR, { recursive: true, force: true })
}
// Create the directory
fs.mkdirSync(SIM_STANDALONE_DIR, { recursive: true })
spinner.text = 'Extracting Sim Studio...'
// Extract the tarball
extract({
file: tarballPath,
cwd: SIM_STANDALONE_DIR,
})
.then(() => {
// Clean up
fs.rmSync(tmpDir, { recursive: true, force: true })
// Install dependencies if needed
if (fs.existsSync(path.join(SIM_STANDALONE_DIR, 'package.json'))) {
spinner.text = 'Installing dependencies...'
try {
execSync('npm install --production', {
cwd: SIM_STANDALONE_DIR,
stdio: 'ignore',
})
} catch (error) {
spinner.warn('Error installing dependencies, but trying to continue...')
}
}
// Write version file
fs.writeFileSync(
SIM_VERSION_FILE,
JSON.stringify({ version: STANDALONE_VERSION, date: new Date().toISOString() })
)
spinner.succeed('Sim Studio downloaded successfully')
resolve()
})
.catch((err) => {
spinner.fail('Failed to extract Sim Studio')
reject(err)
})
})
})
.on('error', (err) => {
spinner.fail('Network error')
reject(err)
})
})
}