feat(npx): added scaffolding for 'npx sim' command that relies on localStorage only, can be ran from anywhere with Nodejs

This commit is contained in:
Waleed Latif
2025-03-04 11:45:55 -08:00
parent d309176c5e
commit b3347afa4e
34 changed files with 3835 additions and 90 deletions

101
packages/@sim/cli/README.md Normal file
View File

@@ -0,0 +1,101 @@
# Sim Studio CLI
The Sim Studio CLI provides a convenient way to run Sim Studio directly from your terminal without needing to set up a database or complex environment.
## Quick Start
```bash
# Run Sim Studio with default settings
npx sim
# Start with custom port
npx sim start -p 8080
# Get help
npx sim help
```
## Features
- **Zero Configuration**: Get started immediately with `npx sim`
- **Local Storage**: Works entirely in the browser, no database required
- **Persistence**: Your workflows and data persist between sessions
- **Familiar Experience**: All the power of Sim Studio in a simplified package
## Commands
- `sim` - Start Sim Studio with default settings
- `sim start` - Start Sim Studio with options
- `sim version` - Display version information
- `sim help` - Show help and usage information
## Options
- `-p, --port <port>` - Specify port (default: 3000)
- `-d, --debug` - Enable debug mode
- `-v, --version` - Show version information
- `-h, --help` - Show help information
## Local Storage Mode
When running Sim Studio via the CLI, all data is stored using the browser's localStorage. This means:
- Your workflows persist between browser sessions
- No database configuration is required
- Data is stored locally on your device
- Multiple users can't share the same workflows (single-user mode)
## Advanced Usage
If you need multi-user capabilities or want to store data in a database, consider:
1. Using the Docker setup in the main repository
2. Setting up a full Sim Studio environment with PostgreSQL
3. Deploying to Vercel with a database
## For Developers: Building & Publishing the CLI
### Release Checklist
1. ✅ Update the CLI code with your changes
2. ✅ Bump the version in `package.json`
3. ✅ Build the standalone version:
```
npm run build:cli
```
4. ✅ Upload the generated `sim-standalone.tar.gz` to GitHub releases
5. ✅ Update the `DOWNLOAD_URL` constant in `packages/@sim/cli/src/commands/start.ts` to point to the new release URL
6. ✅ Commit all changes
7. ✅ Publish to npm:
```
npm run cli:publish
```
### About the Standalone Version
The standalone version is a pre-built and bundled version of Sim Studio that can run without a database or complex setup. It includes:
- A pre-built static export of the Next.js application
- A simple Express server to serve the static files
- Configuration to use browser localStorage for data persistence
This allows users to quickly try Sim Studio with a simple `npx sim` command without installing anything else.
### Testing the CLI Locally
To test the CLI locally:
```bash
# Build the CLI
npm run cli:build
# Run the CLI directly
npm run cli:start
# Or use the dev script
npm run cli:dev
```
## Need Help?
Visit our [documentation](https://github.com/yourusername/sim) or open an issue on GitHub.

14
packages/@sim/cli/bin/sim.js Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env node
// This file is the entry point for the 'sim' command
try {
require('../dist/index.js')
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
console.error('Sim CLI has not been built. Please run npm run build first.')
process.exit(1)
} else {
console.error('An error occurred while starting Sim CLI:', error)
process.exit(1)
}
}

View File

@@ -0,0 +1,4 @@
/**
* Help command displays the logo and usage information
*/
export declare function help(): void;

43
packages/@sim/cli/dist/commands/help.js vendored Normal file
View File

@@ -0,0 +1,43 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.help = help;
const chalk_1 = __importDefault(require("chalk"));
const logo_1 = require("../utils/logo");
/**
* Help command displays the logo and usage information
*/
function help() {
// Display logo
console.log(logo_1.logo);
// Display help text
console.log(`
${chalk_1.default.bold('USAGE')}
${chalk_1.default.cyan('sim')} Start Sim Studio with default settings
${chalk_1.default.cyan('sim start')} Start Sim Studio with options
${chalk_1.default.cyan('sim version')} Display version information
${chalk_1.default.cyan('sim help')} Show this help information
${chalk_1.default.bold('OPTIONS')}
${chalk_1.default.cyan('-p, --port <port>')} Specify port (default: 3000)
${chalk_1.default.cyan('-d, --debug')} Enable debug mode
${chalk_1.default.cyan('-v, --version')} Show version information
${chalk_1.default.cyan('-h, --help')} Show help information
${chalk_1.default.bold('EXAMPLES')}
${chalk_1.default.gray('# Start with default settings')}
${chalk_1.default.cyan('$ sim')}
${chalk_1.default.gray('# Start on a specific port')}
${chalk_1.default.cyan('$ sim start --port 8080')}
${chalk_1.default.gray('# Start with debug logging')}
${chalk_1.default.cyan('$ sim start --debug')}
${chalk_1.default.bold('DOCUMENTATION')}
${chalk_1.default.gray('For more information:')}
https://github.com/simstudioai/sim
`);
}

View File

@@ -0,0 +1,9 @@
interface StartOptions {
port: string;
debug: boolean;
}
/**
* Start command that launches Sim Studio using local storage
*/
export declare function start(options: StartOptions): Promise<import("child_process").ChildProcess>;
export {};

267
packages/@sim/cli/dist/commands/start.js vendored Normal file
View File

@@ -0,0 +1,267 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const child_process_1 = require("child_process");
const chalk_1 = __importDefault(require("chalk"));
const config_1 = require("../utils/config");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const tar_1 = require("tar");
const https_1 = __importDefault(require("https"));
const fs_2 = require("fs");
const child_process_2 = require("child_process");
// Constants for standalone app
const SIM_HOME_DIR = path_1.default.join(os_1.default.homedir(), '.sim-studio');
const SIM_STANDALONE_DIR = path_1.default.join(SIM_HOME_DIR, 'standalone');
const SIM_VERSION_FILE = path_1.default.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
*/
async function start(options) {
// Update config with provided options
config_1.config.set('port', options.port);
config_1.config.set('debug', options.debug);
config_1.config.set('lastRun', new Date().toISOString());
const port = options.port || '3000';
const debug = options.debug || false;
// Dynamically import ora
const oraModule = await Promise.resolve().then(() => __importStar(require('ora')));
const ora = oraModule.default;
// Show starting message
const spinner = ora(`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',
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 = (0, child_process_1.spawn)('npm', ['run', 'dev'], {
env,
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_1.default.existsSync(SIM_HOME_DIR)) {
fs_1.default.mkdirSync(SIM_HOME_DIR, { recursive: true });
}
// Check if we already have the standalone version
let needsDownload = true;
if (fs_1.default.existsSync(SIM_VERSION_FILE)) {
try {
const versionInfo = JSON.parse(fs_1.default.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_1.default.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_1.default.existsSync(SIM_STANDALONE_DIR) || !fs_1.default.existsSync(path_1.default.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_1.default.existsSync(SIM_VERSION_FILE)) {
fs_1.default.unlinkSync(SIM_VERSION_FILE);
}
process.exit(1);
}
// Start the standalone Node.js server
const standaloneEnv = {
...env,
SIM_STUDIO_PORT: port,
};
simProcess = (0, child_process_1.spawn)('node', ['server.js'], {
cwd: SIM_STANDALONE_DIR,
env: standaloneEnv,
stdio: 'inherit',
shell: true
});
}
// Successful start
spinner.succeed(`Sim Studio is running on ${chalk_1.default.cyan(`http://localhost:${port}`)}`);
console.log(`
${chalk_1.default.green('✓')} Using local storage mode - your data will be stored in the browser
${chalk_1.default.green('✓')} Any changes will be persisted between sessions through localStorage
${chalk_1.default.yellow('i')} Press ${chalk_1.default.bold('Ctrl+C')} to stop the server
`);
// Handle process termination
process.on('SIGINT', () => {
console.log(`\n${chalk_1.default.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_1.default.red('Error:'), error instanceof Error ? error.message : error);
process.exit(1);
}
}
/**
* Checks if we're running in a Sim Studio project directory
*/
function checkIfInProjectDirectory() {
// Check if we have package.json that looks like a Sim Studio project
try {
const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
if (fs_1.default.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs_1.default.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_1.default.join(process.cwd(), 'next.config.js');
const nextTsConfigPath = path_1.default.join(process.cwd(), 'next.config.ts');
if (fs_1.default.existsSync(nextConfigPath) || fs_1.default.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) {
return new Promise((resolve, reject) => {
// Create temp directory
const tmpDir = path_1.default.join(os_1.default.tmpdir(), `sim-download-${Date.now()}`);
fs_1.default.mkdirSync(tmpDir, { recursive: true });
const tarballPath = path_1.default.join(tmpDir, 'sim-standalone.tar.gz');
const file = (0, fs_2.createWriteStream)(tarballPath);
spinner.text = 'Downloading Sim Studio...';
// Download the tarball
https_1.default.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_1.default.existsSync(SIM_STANDALONE_DIR)) {
fs_1.default.rmSync(SIM_STANDALONE_DIR, { recursive: true, force: true });
}
// Create the directory
fs_1.default.mkdirSync(SIM_STANDALONE_DIR, { recursive: true });
spinner.text = 'Extracting Sim Studio...';
// Extract the tarball
(0, tar_1.extract)({
file: tarballPath,
cwd: SIM_STANDALONE_DIR
}).then(() => {
// Clean up
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
// Install dependencies if needed
if (fs_1.default.existsSync(path_1.default.join(SIM_STANDALONE_DIR, 'package.json'))) {
spinner.text = 'Installing dependencies...';
try {
(0, child_process_2.execSync)('npm install --production', {
cwd: SIM_STANDALONE_DIR,
stdio: 'ignore'
});
}
catch (error) {
spinner.warn('Error installing dependencies, but trying to continue...');
}
}
// Save version info
fs_1.default.writeFileSync(SIM_VERSION_FILE, JSON.stringify({ version: STANDALONE_VERSION, installedAt: new Date().toISOString() }));
resolve();
}).catch((err) => {
spinner.fail('Extraction failed');
reject(err);
});
});
}).on('error', (err) => {
fs_1.default.unlink(tarballPath, () => { });
spinner.fail('Download failed');
reject(err);
});
});
}

View File

@@ -0,0 +1,4 @@
/**
* Version command displays the current version of the CLI
*/
export declare function version(): void;

View File

@@ -0,0 +1,19 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = version;
const chalk_1 = __importDefault(require("chalk"));
/**
* Version command displays the current version of the CLI
*/
function version() {
const pkg = require('../../package.json');
console.log(`
${chalk_1.default.bold('Sim Studio CLI')} ${chalk_1.default.green(`v${pkg.version}`)}
${chalk_1.default.gray('Platform:')} ${process.platform}
${chalk_1.default.gray('Node Version:')} ${process.version}
${chalk_1.default.gray('CLI Path:')} ${__dirname}
`);
}

2
packages/@sim/cli/dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

68
packages/@sim/cli/dist/index.js vendored Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chalk_1 = __importDefault(require("chalk"));
const commander_1 = require("commander");
const update_notifier_1 = __importDefault(require("update-notifier"));
const start_1 = require("./commands/start");
const help_1 = require("./commands/help");
const version_1 = require("./commands/version");
const logo_1 = require("./utils/logo");
const config_1 = require("./utils/config");
// Package info for version checking
const pkg = require('../package.json');
// Check for updates
(0, update_notifier_1.default)({ pkg }).notify();
// Create program
const program = new commander_1.Command();
// Initialize CLI
async function main() {
// Configure the CLI
program
.name('sim')
.description('Sim Studio CLI')
.version(pkg.version, '-v, --version', 'Output the current version')
.helpOption('-h, --help', 'Display help for command')
.on('--help', () => (0, help_1.help)())
.action(() => {
// Default command (no args) runs start with default options
(0, start_1.start)({ port: config_1.config.get('port'), debug: config_1.config.get('debug') });
});
// Start command
program
.command('start')
.description('Start Sim Studio with local storage')
.option('-p, --port <port>', 'Port to run on', config_1.config.get('port'))
.option('-d, --debug', 'Enable debug mode', config_1.config.get('debug'))
.action((options) => {
(0, start_1.start)(options);
});
// Version command
program
.command('version')
.description('Show detailed version information')
.action(() => {
(0, version_1.version)();
});
// Help command
program
.command('help')
.description('Display help information')
.action(() => {
(0, help_1.help)();
});
// Display logo if not in help mode
if (!process.argv.includes('--help') && !process.argv.includes('-h')) {
console.log(logo_1.logo);
}
// Parse arguments
program.parse(process.argv);
}
// Run the CLI
main().catch((error) => {
console.error(chalk_1.default.red('Error:'), error);
process.exit(1);
});

View File

@@ -0,0 +1,8 @@
import Conf from 'conf';
interface ConfigSchema {
port: string;
debug: boolean;
lastRun: string;
}
export declare const config: Conf<ConfigSchema>;
export {};

16
packages/@sim/cli/dist/utils/config.js vendored Normal file
View File

@@ -0,0 +1,16 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.config = void 0;
const conf_1 = __importDefault(require("conf"));
// Create a config instance with default values
exports.config = new conf_1.default({
projectName: 'sim-studio',
defaults: {
port: '3000',
debug: false,
lastRun: new Date().toISOString(),
},
});

View File

@@ -0,0 +1,4 @@
/**
* ASCII art logo for Sim Studio
*/
export declare const logo: string;

22
packages/@sim/cli/dist/utils/logo.js vendored Normal file
View File

@@ -0,0 +1,22 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.logo = void 0;
const chalk_1 = __importDefault(require("chalk"));
/**
* ASCII art logo for Sim Studio
*/
exports.logo = `
${chalk_1.default.bold(chalk_1.default.magenta(`
███████╗██╗███╗ ███╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
██╔════╝██║████╗ ████║ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
███████╗██║██╔████╔██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
╚════██║██║██║╚██╔╝██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
███████║██║██║ ╚═╝ ██║ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
╚══════╝╚═╝╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
`))}
${chalk_1.default.cyan('Build, optimize, and test agent workflows with a powerful visual interface')}
`;

View File

@@ -0,0 +1,54 @@
{
"name": "@sim/cli",
"version": "0.1.0",
"description": "CLI tool for Sim Studio - easily start, build and test agent workflows",
"license": "MIT",
"author": "Sim Studio Team",
"main": "dist/index.js",
"type": "commonjs",
"bin": {
"sim": "./bin/sim.js"
},
"files": [
"bin",
"dist",
"README.md"
],
"scripts": {
"build": "tsc",
"start": "node bin/sim.js",
"dev": "ts-node src/index.ts",
"clean": "rimraf dist",
"prepublishOnly": "npm run clean && npm run build"
},
"keywords": [
"sim",
"sim-studio",
"workflow",
"automation",
"cli",
"agent"
],
"dependencies": {
"chalk": "^4.1.2",
"commander": "^11.1.0",
"conf": "^10.2.0",
"dotenv": "^16.4.7",
"inquirer": "^8.2.6",
"ora": "^8.2.0",
"tar": "^6.2.1",
"update-notifier": "^5.1.0"
},
"devDependencies": {
"@types/inquirer": "^8.2.10",
"@types/node": "^20.11.30",
"@types/tar": "^6.1.11",
"@types/update-notifier": "^5.1.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=16.0.0"
}
}

View File

@@ -0,0 +1,39 @@
import chalk from 'chalk'
import { logo } from '../utils/logo'
/**
* Help command displays the logo and usage information
*/
export function help() {
// Display logo
console.log(logo)
// Display help text
console.log(`
${chalk.bold('USAGE')}
${chalk.cyan('sim')} Start Sim Studio with default settings
${chalk.cyan('sim start')} Start Sim Studio with options
${chalk.cyan('sim version')} Display version information
${chalk.cyan('sim help')} Show this help information
${chalk.bold('OPTIONS')}
${chalk.cyan('-p, --port <port>')} Specify port (default: 3000)
${chalk.cyan('-d, --debug')} Enable debug mode
${chalk.cyan('-v, --version')} Show version information
${chalk.cyan('-h, --help')} Show help information
${chalk.bold('EXAMPLES')}
${chalk.gray('# Start with default settings')}
${chalk.cyan('$ sim')}
${chalk.gray('# Start on a specific port')}
${chalk.cyan('$ sim start --port 8080')}
${chalk.gray('# Start with debug logging')}
${chalk.cyan('$ sim start --debug')}
${chalk.bold('DOCUMENTATION')}
${chalk.gray('For more information:')}
https://github.com/simstudioai/sim
`)
}

View File

@@ -0,0 +1,286 @@
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 type { Ora } from 'ora'
import os from 'os'
import path from 'path'
import { extract } from 'tar'
import { config } from '../utils/config'
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
// Dynamically import ora
const oraModule = await import('ora')
const ora = oraModule.default
// Show starting message
const spinner = ora(`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',
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,
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,
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: Ora): 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...')
}
}
// Save version info
fs.writeFileSync(
SIM_VERSION_FILE,
JSON.stringify({
version: STANDALONE_VERSION,
installedAt: new Date().toISOString(),
})
)
resolve()
})
.catch((err: Error) => {
spinner.fail('Extraction failed')
reject(err)
})
})
})
.on('error', (err: Error) => {
fs.unlink(tarballPath, () => {})
spinner.fail('Download failed')
reject(err)
})
})
}

View File

@@ -0,0 +1,15 @@
import chalk from 'chalk'
/**
* Version command displays the current version of the CLI
*/
export function version() {
const pkg = require('../../package.json')
console.log(`
${chalk.bold('Sim Studio CLI')} ${chalk.green(`v${pkg.version}`)}
${chalk.gray('Platform:')} ${process.platform}
${chalk.gray('Node Version:')} ${process.version}
${chalk.gray('CLI Path:')} ${__dirname}
`)
}

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env node
import chalk from 'chalk'
import { Command } from 'commander'
import updateNotifier from 'update-notifier'
import { help } from './commands/help'
import { start } from './commands/start'
import { version } from './commands/version'
import { config } from './utils/config'
import { logo } from './utils/logo'
// Package info for version checking
const pkg = require('../package.json')
// Check for updates
updateNotifier({ pkg }).notify()
// Create program
const program = new Command()
// Initialize CLI
async function main() {
// Configure the CLI
program
.name('sim')
.description('Sim Studio CLI')
.version(pkg.version, '-v, --version', 'Output the current version')
.helpOption('-h, --help', 'Display help for command')
.on('--help', () => help())
.action(() => {
// Default command (no args) runs start with default options
start({ port: config.get('port'), debug: config.get('debug') })
})
// Start command
program
.command('start')
.description('Start Sim Studio with local storage')
.option('-p, --port <port>', 'Port to run on', config.get('port'))
.option('-d, --debug', 'Enable debug mode', config.get('debug'))
.action((options) => {
start(options)
})
// Version command
program
.command('version')
.description('Show detailed version information')
.action(() => {
version()
})
// Help command
program
.command('help')
.description('Display help information')
.action(() => {
help()
})
// Display logo if not in help mode
if (!process.argv.includes('--help') && !process.argv.includes('-h')) {
console.log(logo)
}
// Parse arguments
program.parse(process.argv)
}
// Run the CLI
main().catch((error) => {
console.error(chalk.red('Error:'), error)
process.exit(1)
})

View File

@@ -0,0 +1,18 @@
import Conf from 'conf'
// Config schema definition
interface ConfigSchema {
port: string
debug: boolean
lastRun: string
}
// Create a config instance with default values
export const config = new Conf<ConfigSchema>({
projectName: 'sim-studio',
defaults: {
port: '3000',
debug: false,
lastRun: new Date().toISOString(),
},
})

View File

@@ -0,0 +1,19 @@
import chalk from 'chalk'
/**
* ASCII art logo for Sim Studio
*/
export const logo = `
${chalk.bold(
chalk.magenta(`
███████╗██╗███╗ ███╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
██╔════╝██║████╗ ████║ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
███████╗██║██╔████╔██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
╚════██║██║██║╚██╔╝██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
███████║██║██║ ╚═╝ ██║ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
╚══════╝╚═╝╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
`)
)}
${chalk.cyan('Build, optimize, and test agent workflows with a powerful visual interface')}
`

View File

@@ -0,0 +1,16 @@
{
"name": "sim-studio-standalone",
"version": "0.1.0",
"private": true,
"description": "Standalone server for Sim Studio",
"main": "server.js",
"dependencies": {
"express": "^4.18.2"
},
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"start": "node server.js"
}
}

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* Sim Studio Standalone Server
*
* This is a simplified server that serves the pre-built Sim Studio app
* and enables localStorage mode automatically.
*/
const express = require('express')
const path = require('path')
const fs = require('fs')
const { createServer } = require('http')
const { parse } = require('url')
// Configuration
const PORT = process.env.SIM_STUDIO_PORT || 3000
const PUBLIC_DIR = path.join(__dirname, 'public')
const HTML_FILE = path.join(PUBLIC_DIR, 'index.html')
// Create Express app
const app = express()
// Set localStorage environment variable in HTML
const injectLocalStorageScript = (html) => {
const script = `
<script>
// Set localStorage flag for Sim Studio
localStorage.setItem('USE_LOCAL_STORAGE', 'true');
console.log('Sim Studio running in local storage mode');
</script>
`
// Insert script right before the closing </head> tag
return html.replace('</head>', `${script}</head>`)
}
// Middleware to inject localStorage flag
app.use((req, res, next) => {
if (req.path === '/' || req.path.endsWith('.html')) {
const originalSend = res.send
res.send = function (body) {
if (typeof body === 'string' && body.includes('</head>')) {
body = injectLocalStorageScript(body)
}
return originalSend.call(this, body)
}
}
next()
})
// Serve static files
app.use(express.static(PUBLIC_DIR))
// SPA fallback - all routes not matched should serve index.html
app.get('*', (req, res) => {
res.sendFile(HTML_FILE)
})
// Start the server
app.listen(PORT, () => {
console.log(`
┌────────────────────────────────────────────────────┐
│ │
│ 🚀 Sim Studio is running in standalone mode! │
│ │
│ 🌐 Local: http://localhost:${PORT} ${PORT.toString().length < 4 ? ' ' : ''}
│ │
│ 💾 Using localStorage for all data │
│ 🔄 All changes will be saved in your browser │
│ │
│ Press Ctrl+C to stop the server │
│ │
└────────────────────────────────────────────────────┘
`)
})

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"esModuleInterop": true,
"declaration": true,
"outDir": "./dist",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}