feat: Full SDK implementations for 8 languages (sync + async)

Complete SDK implementations with full API surface:
- Python: sync/async with all 43 functions
- Go: sync/async with full API
- Java: sync/async with full API
- JavaScript: sync/async with full API
- PHP: sync/async with full API
- Ruby: sync/async with full API
- Rust: sync/async with full API

Added:
- clients/README.md - SDK overview
- clients/CLI_SPEC.md - CLI specification

Removed:
- un_deno.ts (moved to clients/typescript/)
This commit is contained in:
russell@unturf.com 2026-01-15 18:35:43 -05:00
parent 1fda4110b1
commit 6d91e987ce
17 changed files with 14170 additions and 637 deletions

269
clients/CLI_SPEC.md Normal file
View file

@ -0,0 +1,269 @@
# Unsandbox CLI Specification
**Every SDK implementation MUST include a CLI interface identical to un.c.**
One file. One CLI spec. 42+ languages. All implementing the same API.
## Core Principle
Each SDK file (un.py, un.js, un.rb, etc.) is BOTH:
1. **A library** - Can be imported/required by other code
2. **A CLI tool** - Can be executed directly from command line
```bash
# Library usage
python -c "from un import execute_code; print(execute_code('python', 'print(1)'))"
# CLI usage
python un.py script.py
python un.py -s bash 'echo hello'
python un.py session --tmux
```
## CLI Entry Points
Each language must detect when run as main:
```python
# Python
if __name__ == "__main__":
cli_main()
```
```javascript
// JavaScript (Node.js)
if (require.main === module) {
cliMain();
}
```
```ruby
# Ruby
if __FILE__ == $0
cli_main
end
```
```go
// Go - separate main.go that imports the library
func main() {
un.CliMain()
}
```
## Command Structure
```
un [options] <source_file> # Execute code file
un session [options] # Interactive session
un service [options] # Manage services
un snapshot [options] # Manage snapshots
un key # Check API key
```
## Global Options
| Option | Short | Description |
|--------|-------|-------------|
| `--shell LANG` | `-s` | Language for inline code |
| `--env KEY=VAL` | `-e` | Set environment variable |
| `--file FILE` | `-f` | Add input file to /tmp/ |
| `--file-path FILE` | `-F` | Add input file with path preserved |
| `--artifacts` | `-a` | Return compiled artifacts |
| `--output DIR` | `-o` | Output directory for artifacts |
| `--public-key KEY` | `-p` | API public key |
| `--secret-key KEY` | `-k` | API secret key |
| `--network MODE` | `-n` | Network: zerotrust or semitrusted |
| `--vcpu N` | `-v` | vCPU count (1-8) |
| `--yes` | `-y` | Skip confirmation prompts |
| `--help` | `-h` | Show help |
## Execute Command (Default)
```bash
un script.py # Execute Python script
un -s bash 'echo hello' # Inline bash command
un -e DEBUG=1 script.py # With environment variable
un -f data.csv process.py # With input file
un -a -o ./bin main.c # Save compiled artifacts
un -n semitrusted crawler.py # With network access
```
## Session Command
```bash
un session # Interactive bash
un session --shell python3 # Python REPL
un session --tmux # Persistent session (can reconnect)
un session --screen # Persistent with screen
un session --list # List active sessions
un session --attach ID # Reconnect to session
un session --kill ID # Terminate session
un session --freeze ID # Pause session
un session --unfreeze ID # Resume session
un session --boost ID # Add resources
un session --unboost ID # Remove boost
un session --snapshot ID # Create snapshot
un session -n semitrusted # With network access
```
### Session Options
| Option | Description |
|--------|-------------|
| `--shell SHELL` | Shell/REPL to use (default: bash) |
| `--list`, `-l` | List active sessions |
| `--attach ID` | Reconnect to existing session |
| `--kill ID` | Terminate a session |
| `--freeze ID` | Pause session |
| `--unfreeze ID` | Resume session |
| `--boost ID` | Add vCPUs/RAM |
| `--unboost ID` | Remove boost |
| `--tmux` | Enable persistence with tmux |
| `--screen` | Enable persistence with screen |
| `--snapshot ID` | Create snapshot |
| `--snapshot-name NAME` | Name for snapshot |
| `--hot` | Live snapshot (no freeze) |
| `--audit` | Record session |
## Service Command
```bash
un service --list # List all services
un service --name myapp --ports 80 --bootstrap "python -m http.server 80"
un service --info ID # Get service details
un service --logs ID # Get bootstrap logs
un service --freeze ID # Pause service
un service --unfreeze ID # Resume service
un service --destroy ID # Delete service
un service --lock ID # Prevent deletion
un service --unlock ID # Allow deletion
un service --execute ID 'cmd' # Run command in service
un service --redeploy ID # Re-run bootstrap
un service --snapshot ID # Create snapshot
```
### Service Options
| Option | Description |
|--------|-------------|
| `--name NAME` | Service name (creates new) |
| `--ports PORTS` | Comma-separated ports |
| `--domains DOMAINS` | Custom domains |
| `--type TYPE` | Service type (minecraft, tcp, udp) |
| `--bootstrap CMD` | Bootstrap command |
| `--bootstrap-file FILE` | Bootstrap from file |
| `--env-file FILE` | Load env from .env file |
| `--list`, `-l` | List all services |
| `--info ID` | Get service details |
| `--logs ID` | Get all logs |
| `--tail ID` | Get last 9000 lines |
| `--freeze ID` | Pause service |
| `--unfreeze ID` | Resume service |
| `--destroy ID` | Delete service |
| `--lock ID` | Prevent deletion |
| `--unlock ID` | Allow deletion |
| `--resize ID` | Resize (with --vcpu) |
| `--redeploy ID` | Re-run bootstrap |
| `--execute ID CMD` | Run command |
| `--snapshot ID` | Create snapshot |
### Service Environment Vault
```bash
un service env status ID # Show vault status
un service env set ID # Set from --env-file or stdin
un service env export ID # Export to stdout
un service env delete ID # Delete vault
```
## Snapshot Command
```bash
un snapshot --list # List all snapshots
un snapshot --info ID # Get details
un snapshot --delete ID # Delete snapshot
un snapshot --lock ID # Prevent deletion
un snapshot --unlock ID # Allow deletion
un snapshot --clone ID # Clone to new session/service
un snapshot --clone ID --type service --name myapp --ports 80
```
### Snapshot Options
| Option | Description |
|--------|-------------|
| `--list`, `-l` | List all snapshots |
| `--info ID` | Get snapshot details |
| `--delete ID` | Delete snapshot |
| `--lock ID` | Prevent deletion |
| `--unlock ID` | Allow deletion |
| `--clone ID` | Clone snapshot |
| `--type TYPE` | Clone type: session or service |
| `--name NAME` | Name for cloned service |
| `--shell SHELL` | Shell for cloned session |
| `--ports PORTS` | Ports for cloned service |
## Key Command
```bash
un key # Check API key validity
```
## Output Formatting
### Execute Output
```
stdout content here
---
Exit code: 0
Execution time: 123ms
```
### List Output (sessions, services, snapshots)
```
ID NAME STATUS CREATED
abc123-def456-789 my-session running 2024-01-15 10:30:00
```
### Error Output
```
Error: <message>
```
Errors go to stderr, exit code 1.
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments |
| 3 | Authentication error |
| 4 | API error |
| 5 | Timeout |
## Implementation Requirements
1. **Argument parsing** - Use language-appropriate library (argparse, commander, clap, etc.)
2. **Subcommands** - session, service, snapshot, key
3. **Consistent output** - Same format across all languages
4. **Error handling** - Proper exit codes, errors to stderr
5. **Credential resolution** - 4-tier system (-p/-k flags > env > ~/.unsandbox > ./accounts.csv)
## Available Shells/REPLs
```
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython, node, ruby, irb, lua, php, perl
guile, ghci, erl, iex, sbcl, clisp, r, julia, clojure
```
## File Size Target
With CLI included, each SDK should be approximately:
- **2,000-3,000 lines** for higher-level languages (Python, Ruby, JavaScript)
- **3,000-4,000 lines** for verbose languages (Java, Go, Rust)
Reference: un.c = 6,354 lines (includes manual HTTP, JSON, crypto)

318
clients/README.md Normal file
View file

@ -0,0 +1,318 @@
# Unsandbox SDK Clients
Multi-language SDK implementations for the Unsandbox API.
**Each SDK is BOTH a library AND a CLI tool** - see [CLI_SPEC.md](CLI_SPEC.md) for the full CLI specification.
```bash
# Library usage
python -c "from un import execute_code; print(execute_code('python', 'print(1)'))"
# CLI usage (identical across all 42+ languages)
python un.py script.py
python un.py -s bash 'echo hello'
python un.py session --tmux
python un.py service --list
```
## Directory Structure
```
clients/
├── python/
│ ├── sync/src/un.py # Synchronous (requests)
│ └── async/src/un_async.py # Asynchronous (aiohttp)
├── javascript/
│ ├── sync/src/un.js # Synchronous (https)
│ └── async/src/un_async.js # Asynchronous (fetch)
├── go/
│ ├── sync/src/un.go # Synchronous (net/http)
│ └── async/src/un_async.go # Asynchronous (goroutines)
├── java/
│ ├── sync/src/Un.java # Synchronous (HttpURLConnection)
│ └── async/src/UnsandboxAsync.java # Asynchronous (CompletableFuture)
├── ruby/
│ ├── sync/src/un.rb # Synchronous (net/http)
│ └── async/src/un_async.rb # Asynchronous (Future)
├── rust/
│ ├── sync/src/lib.rs # Synchronous (reqwest blocking)
│ └── async/src/lib.rs # Asynchronous (reqwest + tokio)
├── php/
│ ├── sync/src/un.php # Synchronous (cURL)
│ └── async/src/UnsandboxAsync.php # Asynchronous (Guzzle promises)
└── c/
└── src/unsandbox.c # Reference implementation
```
## API Functions (43 total)
All SDKs implement the complete Unsandbox API:
### Execute & Jobs (6 functions)
| Function | Description | Endpoint |
|----------|-------------|----------|
| `execute_code` | Execute code synchronously | POST /execute |
| `execute_async` | Execute code, return job ID | POST /execute (async mode) |
| `get_job` | Get job status | GET /jobs/{id} |
| `wait_for_job` | Poll until completion | GET /jobs/{id} (polling) |
| `cancel_job` | Cancel running job | DELETE /jobs/{id} |
| `list_jobs` | List all jobs | GET /jobs |
### Sessions (9 functions)
| Function | Description | Endpoint |
|----------|-------------|----------|
| `list_sessions` | List all sessions | GET /sessions |
| `get_session` | Get session details | GET /sessions/{id} |
| `create_session` | Create interactive session | POST /sessions |
| `delete_session` | Terminate session | DELETE /sessions/{id} |
| `freeze_session` | Pause session | POST /sessions/{id}/freeze |
| `unfreeze_session` | Resume session | POST /sessions/{id}/unfreeze |
| `boost_session` | Add vCPUs | POST /sessions/{id}/boost |
| `unboost_session` | Remove boost | POST /sessions/{id}/unboost |
| `shell_session` | Execute shell command | POST /sessions/{id}/shell |
### Services (16 functions)
| Function | Description | Endpoint |
|----------|-------------|----------|
| `list_services` | List all services | GET /services |
| `create_service` | Create persistent service | POST /services |
| `get_service` | Get service details | GET /services/{id} |
| `update_service` | Update/resize service | PATCH /services/{id} |
| `delete_service` | Destroy service | DELETE /services/{id} |
| `freeze_service` | Pause service | POST /services/{id}/freeze |
| `unfreeze_service` | Resume service | POST /services/{id}/unfreeze |
| `lock_service` | Prevent deletion | POST /services/{id}/lock |
| `unlock_service` | Allow deletion | POST /services/{id}/unlock |
| `get_service_logs` | Get bootstrap logs | GET /services/{id}/logs |
| `get_service_env` | Get env vault status | GET /services/{id}/env |
| `set_service_env` | Set environment vars | PUT /services/{id}/env |
| `delete_service_env` | Delete env vars | DELETE /services/{id}/env |
| `export_service_env` | Export decrypted env | POST /services/{id}/env/export |
| `redeploy_service` | Re-run bootstrap | POST /services/{id}/redeploy |
| `execute_in_service` | Run command in service | POST /services/{id}/execute |
### Snapshots (8 functions)
| Function | Description | Endpoint |
|----------|-------------|----------|
| `list_snapshots` | List all snapshots | GET /snapshots |
| `get_snapshot` | Get snapshot details | GET /snapshots/{id} |
| `delete_snapshot` | Delete snapshot | DELETE /snapshots/{id} |
| `session_snapshot` | Snapshot a session | POST /sessions/{id}/snapshot |
| `service_snapshot` | Snapshot a service | POST /services/{id}/snapshot |
| `restore_snapshot` | Restore from snapshot | POST /snapshots/{id}/restore |
| `lock_snapshot` | Prevent deletion | POST /snapshots/{id}/lock |
| `unlock_snapshot` | Allow deletion | POST /snapshots/{id}/unlock |
| `clone_snapshot` | Clone snapshot | POST /snapshots/{id}/clone |
### Utilities (4 functions)
| Function | Description | Endpoint |
|----------|-------------|----------|
| `get_languages` | Get supported languages | GET /languages (cached 1hr) |
| `detect_language` | Detect from filename | Local (no API call) |
| `validate_keys` | Validate API credentials | POST /keys/validate |
| `image` | Generate AI images | POST /image |
## Authentication
### 4-Tier Credential Resolution
All SDKs resolve credentials in this priority order:
1. **Function arguments** (highest priority)
```python
result = execute_code("python", code, public_key="pk-xxx", secret_key="sk-xxx")
```
2. **Environment variables**
```bash
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxx"
```
3. **Home directory config**
```
~/.unsandbox/accounts.csv
Format: public_key,secret_key (one per line)
```
4. **Local directory config**
```
./accounts.csv
Format: public_key,secret_key (one per line)
```
### HMAC-SHA256 Request Signing
Every API request includes:
```
Authorization: Bearer <public_key>
X-Timestamp: <unix_seconds>
X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
```
Example signature message:
```
1704067200:POST:/execute:{"language":"python","code":"print(1)"}
```
## Caching
### Languages Cache
All SDKs cache the `/languages` response:
- **Location**: `~/.unsandbox/languages.json`
- **TTL**: 1 hour (3600 seconds)
- **Behavior**: Check cache freshness before API call
## Line Count Comparison
### Why un.c is 6,354 lines vs Python's 1,714 lines
The reference C implementation includes infrastructure that high-level languages get from standard libraries:
| Component | un.c (lines) | Python | Notes |
|-----------|--------------|--------|-------|
| **API Functions** | ~2,500 | ~1,500 | Similar complexity |
| **CLI main()** | 1,306 | 0 | SDK is library-only |
| **CLI help/usage** | 154 | 0 | No CLI in SDK |
| **HTTP client** | 574 | 0 | `requests` library |
| **JSON parsing** | ~200 | 0 | `json` module |
| **HMAC crypto** | ~100 | 0 | `hmac` module |
| **Memory management** | ~300 | 0 | Garbage collected |
| **String utilities** | ~200 | 0 | Built-in |
| **Total** | **6,354** | **1,714** | 3.7x difference |
### What Python Gets "For Free"
```python
import requests # Replaces ~574 lines of curl code
import json # Replaces ~200 lines of JSON parsing
import hmac # Replaces ~100 lines of crypto
import hashlib # Replaces SHA-256 implementation
```
### Feature Parity
Despite the line count difference, all SDKs implement:
- ✅ All 43 API functions
- ✅ 4-tier credential resolution
- ✅ HMAC-SHA256 request signing
- ✅ 1-hour languages caching
- ✅ Exponential backoff polling
- ✅ Proper error handling
The C implementation additionally includes:
- Full CLI with argument parsing
- Interactive shell support (WebSocket)
- Output formatting and display
- File input handling
## SDK Size Summary
| Language | Sync | Async | Total | Notes |
|----------|------|-------|-------|-------|
| Python | 1,714 | 1,701 | 3,415 | requests/aiohttp |
| JavaScript | 1,131 | 1,209 | 2,340 | https/fetch |
| Go | 1,080 | 1,642 | 2,722 | net/http + goroutines |
| Java | 1,856 | 1,849 | 3,705 | HttpURLConnection |
| Ruby | 1,264 | 1,543 | 2,807 | net/http + Future |
| Rust | 1,856 | 1,949 | 3,805 | reqwest + tokio |
| PHP | 1,403 | 1,346 | 2,749 | cURL + Guzzle |
| **Total** | | | **21,543** | |
Reference: `un.c` = 6,354 lines (includes CLI)
## Usage Examples
### Python (Sync)
```python
from un import execute_code, list_services, image
# Execute code
result = execute_code("python", 'print("Hello, World!")')
print(result["stdout"])
# List services
services = list_services()
for svc in services:
print(f"{svc['name']}: {svc['status']}")
# Generate AI image
img = image("A sunset over mountains")
print(img["images"][0])
```
### Python (Async)
```python
import asyncio
from un_async import execute_code, list_services
async def main():
result = await execute_code("python", 'print("Hello!")')
print(result["stdout"])
asyncio.run(main())
```
### JavaScript (Sync)
```javascript
const un = require('./un');
const result = un.executeCode("javascript", 'console.log("Hello!")');
console.log(result.stdout);
```
### Go
```go
import "un"
func main() {
creds, _ := un.ResolveCredentials("", "")
result, _ := un.ExecuteCode(creds, "python", `print("Hello")`)
fmt.Println(result.Stdout)
}
```
### Rust
```rust
use un::{execute_code, resolve_credentials};
fn main() -> Result<()> {
let creds = resolve_credentials(None, None)?;
let result = execute_code("python", r#"print("Hello")"#, &creds)?;
println!("{}", result.output);
Ok(())
}
```
## Error Handling
All SDKs define these error types:
| Error | Description |
|-------|-------------|
| `CredentialsError` | No credentials found or invalid |
| `APIError` | API returned error response |
| `TimeoutError` | Job polling exceeded timeout |
| `NetworkError` | Connection failed |
## Contributing
When adding new API endpoints:
1. Update all 14 SDK files (7 languages × 2 variants)
2. Follow existing patterns for authentication and error handling
3. Add proper documentation (docstrings/comments)
4. Test with the SDK test framework
## License
PUBLIC DOMAIN - No license, no warranty.
Part of the permacomputer project: https://permacomputer.com

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1692,6 +1692,49 @@ public class UnsandboxAsync {
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
}
// ========================================================================
// Image Generation API
// ========================================================================
/**
* Generate images from text prompt using AI.
*
* @param prompt Text description of the image to generate
* @param model Model to use (optional, can be null)
* @param size Image size (e.g., "1024x1024")
* @param quality "standard" or "hd"
* @param n Number of images to generate
* @param publicKey API public key (optional)
* @param secretKey API secret key (optional)
* @return CompletableFuture containing Map with "images" (List of base64/URLs) and "created_at"
*/
public static CompletableFuture<Map<String, Object>> image(
String prompt,
String model,
String size,
String quality,
int n,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
String finalSize = (size == null) ? "1024x1024" : size;
String finalQuality = (quality == null) ? "standard" : quality;
int finalN = (n <= 0) ? 1 : n;
Map<String, Object> data = new LinkedHashMap<>();
data.put("prompt", prompt);
data.put("size", finalSize);
data.put("quality", finalQuality);
data.put("n", finalN);
if (model != null && !model.isEmpty()) {
data.put("model", model);
}
return makeRequest("POST", "/image", creds[0], creds[1], data);
}
// ========================================================================
// HTTP Request Helpers
// ========================================================================
@ -1803,4 +1846,840 @@ public class UnsandboxAsync {
return false;
}
}
// ========================================================================
// CLI Implementation
// ========================================================================
/**
* CLI entry point.
*/
public static void main(String[] args) {
try {
cliMain(args);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
}
/**
* CLI main implementation.
*/
public static void cliMain(String[] args) throws Exception {
if (args.length == 0) {
printHelp();
System.exit(0);
}
// Parse global options
String publicKey = null;
String secretKey = null;
String language = null;
String networkMode = "zerotrust";
int vcpu = 1;
List<String> envVars = new ArrayList<>();
List<String> files = new ArrayList<>();
List<String> positionalArgs = new ArrayList<>();
boolean showHelp = false;
int i = 0;
while (i < args.length) {
String arg = args[i];
if (arg.equals("-h") || arg.equals("--help")) {
showHelp = true;
i++;
} else if (arg.equals("-s") || arg.equals("--shell")) {
if (i + 1 >= args.length) {
System.err.println("Error: -s/--shell requires an argument");
System.exit(2);
}
language = args[++i];
i++;
} else if (arg.equals("-p") || arg.equals("--public-key")) {
if (i + 1 >= args.length) {
System.err.println("Error: -p/--public-key requires an argument");
System.exit(2);
}
publicKey = args[++i];
i++;
} else if (arg.equals("-k") || arg.equals("--secret-key")) {
if (i + 1 >= args.length) {
System.err.println("Error: -k/--secret-key requires an argument");
System.exit(2);
}
secretKey = args[++i];
i++;
} else if (arg.equals("-n") || arg.equals("--network")) {
if (i + 1 >= args.length) {
System.err.println("Error: -n/--network requires an argument");
System.exit(2);
}
networkMode = args[++i];
i++;
} else if (arg.equals("-v") || arg.equals("--vcpu")) {
if (i + 1 >= args.length) {
System.err.println("Error: -v/--vcpu requires an argument");
System.exit(2);
}
vcpu = Integer.parseInt(args[++i]);
i++;
} else if (arg.equals("-e") || arg.equals("--env")) {
if (i + 1 >= args.length) {
System.err.println("Error: -e/--env requires an argument");
System.exit(2);
}
envVars.add(args[++i]);
i++;
} else if (arg.equals("-f") || arg.equals("--file")) {
if (i + 1 >= args.length) {
System.err.println("Error: -f/--file requires an argument");
System.exit(2);
}
files.add(args[++i]);
i++;
} else if (arg.startsWith("-")) {
System.err.println("Error: Unknown option: " + arg);
System.exit(2);
} else {
positionalArgs.add(arg);
i++;
}
}
if (showHelp || positionalArgs.isEmpty()) {
printHelp();
System.exit(0);
}
String command = positionalArgs.get(0);
try {
// Route to subcommand handlers
switch (command) {
case "session":
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
break;
case "service":
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
break;
case "snapshot":
handleSnapshot(positionalArgs, publicKey, secretKey);
break;
case "key":
handleKey(publicKey, secretKey);
break;
default:
// Default: execute code
handleExecute(positionalArgs, publicKey, secretKey, language, networkMode, vcpu, envVars, files);
break;
}
} finally {
shutdown();
}
}
private static void printHelp() {
System.out.println("UnsandboxAsync - unsandbox.com CLI (Java Asynchronous SDK)");
System.out.println();
System.out.println("Usage:");
System.out.println(" java UnsandboxAsync [options] <source_file> Execute code file");
System.out.println(" java UnsandboxAsync [options] -s LANG 'code' Execute inline code");
System.out.println(" java UnsandboxAsync session [options] Interactive session");
System.out.println(" java UnsandboxAsync service [options] Manage services");
System.out.println(" java UnsandboxAsync snapshot [options] Manage snapshots");
System.out.println(" java UnsandboxAsync key Check API key");
System.out.println();
System.out.println("Global Options:");
System.out.println(" -s, --shell LANG Language for inline code");
System.out.println(" -e, --env KEY=VAL Set environment variable");
System.out.println(" -f, --file FILE Add input file to /tmp/");
System.out.println(" -p, --public-key KEY API public key");
System.out.println(" -k, --secret-key KEY API secret key");
System.out.println(" -n, --network MODE Network: zerotrust or semitrusted");
System.out.println(" -v, --vcpu N vCPU count (1-8)");
System.out.println(" -h, --help Show help");
System.out.println();
System.out.println("Session Options:");
System.out.println(" --list, -l List active sessions");
System.out.println(" --attach ID Reconnect to session");
System.out.println(" --kill ID Terminate session");
System.out.println(" --freeze ID Pause session");
System.out.println(" --unfreeze ID Resume session");
System.out.println(" --boost ID Add resources");
System.out.println(" --unboost ID Remove boost");
System.out.println(" --snapshot ID Create snapshot");
System.out.println(" --tmux Enable persistence with tmux");
System.out.println(" --screen Enable persistence with screen");
System.out.println(" --shell SHELL Shell/REPL to use");
System.out.println();
System.out.println("Service Options:");
System.out.println(" --list, -l List all services");
System.out.println(" --name NAME Service name (creates new)");
System.out.println(" --ports PORTS Comma-separated ports");
System.out.println(" --bootstrap CMD Bootstrap command");
System.out.println(" --info ID Get service details");
System.out.println(" --logs ID Get all logs");
System.out.println(" --freeze ID Pause service");
System.out.println(" --unfreeze ID Resume service");
System.out.println(" --destroy ID Delete service");
System.out.println(" --lock ID Prevent deletion");
System.out.println(" --unlock ID Allow deletion");
System.out.println(" --execute ID CMD Run command in service");
System.out.println(" --redeploy ID Re-run bootstrap");
System.out.println(" --snapshot ID Create snapshot");
System.out.println();
System.out.println("Service Env Subcommand:");
System.out.println(" java UnsandboxAsync service env status ID Show vault status");
System.out.println(" java UnsandboxAsync service env set ID Set from stdin");
System.out.println(" java UnsandboxAsync service env export ID Export to stdout");
System.out.println(" java UnsandboxAsync service env delete ID Delete vault");
System.out.println();
System.out.println("Snapshot Options:");
System.out.println(" --list, -l List all snapshots");
System.out.println(" --info ID Get snapshot details");
System.out.println(" --delete ID Delete snapshot");
System.out.println(" --lock ID Prevent deletion");
System.out.println(" --unlock ID Allow deletion");
System.out.println(" --clone ID Clone snapshot");
}
private static void handleExecute(
List<String> args,
String publicKey,
String secretKey,
String language,
String networkMode,
int vcpu,
List<String> envVars,
List<String> files
) throws Exception {
String codeOrFile = args.get(0);
String code;
if (language != null) {
// Inline code mode: -s python 'print(1)'
code = codeOrFile;
} else {
// File mode: script.py
Path filePath = Paths.get(codeOrFile);
if (!Files.exists(filePath)) {
System.err.println("Error: File not found: " + codeOrFile);
System.exit(1);
}
code = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
language = detectLanguage(codeOrFile);
if (language == null) {
System.err.println("Error: Cannot detect language from file extension: " + codeOrFile);
System.exit(1);
}
}
Map<String, Object> result = executeCode(language, code, publicKey, secretKey).get();
// Print output
Object stdout = result.get("stdout");
if (stdout != null && !stdout.toString().isEmpty()) {
System.out.print(stdout);
}
Object stderr = result.get("stderr");
if (stderr != null && !stderr.toString().isEmpty()) {
System.err.print(stderr);
}
System.out.println("---");
Object exitCode = result.get("exit_code");
System.out.println("Exit code: " + (exitCode != null ? exitCode : "0"));
Object executionTime = result.get("execution_time_ms");
if (executionTime != null) {
System.out.println("Execution time: " + executionTime + "ms");
}
// Exit with the code's exit code
if (exitCode != null && exitCode instanceof Number) {
int code_exit = ((Number) exitCode).intValue();
if (code_exit != 0) {
System.exit(code_exit);
}
}
}
private static void handleSession(
List<String> args,
String publicKey,
String secretKey,
String networkMode,
int vcpu,
String shell
) throws Exception {
// Parse session-specific options
boolean list = false;
String attachId = null;
String killId = null;
String freezeId = null;
String unfreezeId = null;
String boostId = null;
String unboostId = null;
String snapshotId = null;
String snapshotName = null;
boolean hot = false;
boolean useTmux = false;
boolean useScreen = false;
int i = 1; // Skip "session" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--attach")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --attach requires an ID");
System.exit(2);
}
attachId = args.get(++i);
i++;
} else if (arg.equals("--kill")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --kill requires an ID");
System.exit(2);
}
killId = args.get(++i);
i++;
} else if (arg.equals("--freeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --freeze requires an ID");
System.exit(2);
}
freezeId = args.get(++i);
i++;
} else if (arg.equals("--unfreeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unfreeze requires an ID");
System.exit(2);
}
unfreezeId = args.get(++i);
i++;
} else if (arg.equals("--boost")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --boost requires an ID");
System.exit(2);
}
boostId = args.get(++i);
i++;
} else if (arg.equals("--unboost")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unboost requires an ID");
System.exit(2);
}
unboostId = args.get(++i);
i++;
} else if (arg.equals("--snapshot")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot requires an ID");
System.exit(2);
}
snapshotId = args.get(++i);
i++;
} else if (arg.equals("--snapshot-name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot-name requires a name");
System.exit(2);
}
snapshotName = args.get(++i);
i++;
} else if (arg.equals("--hot")) {
hot = true;
i++;
} else if (arg.equals("--tmux")) {
useTmux = true;
i++;
} else if (arg.equals("--screen")) {
useScreen = true;
i++;
} else if (arg.equals("--shell")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --shell requires a value");
System.exit(2);
}
shell = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> sessions = listSessions(publicKey, secretKey).get();
printSessionList(sessions);
} else if (attachId != null) {
Map<String, Object> session = getSession(attachId, publicKey, secretKey).get();
System.out.println("Session: " + attachId);
printMap(session);
} else if (killId != null) {
deleteSession(killId, publicKey, secretKey).get();
System.out.println("Session terminated: " + killId);
} else if (freezeId != null) {
freezeSession(freezeId, publicKey, secretKey).get();
System.out.println("Session frozen: " + freezeId);
} else if (unfreezeId != null) {
unfreezeSession(unfreezeId, publicKey, secretKey).get();
System.out.println("Session unfrozen: " + unfreezeId);
} else if (boostId != null) {
boostSession(boostId, publicKey, secretKey).get();
System.out.println("Session boosted: " + boostId);
} else if (unboostId != null) {
unboostSession(unboostId, publicKey, secretKey).get();
System.out.println("Session unboosted: " + unboostId);
} else if (snapshotId != null) {
String snapId = sessionSnapshot(snapshotId, publicKey, secretKey, snapshotName, hot).get();
System.out.println("Snapshot created: " + snapId);
} else {
// Create new session
Map<String, Object> opts = new LinkedHashMap<>();
opts.put("network_mode", networkMode);
if (vcpu > 1) {
opts.put("vcpu", vcpu);
}
if (useTmux) {
opts.put("multiplexer", "tmux");
} else if (useScreen) {
opts.put("multiplexer", "screen");
}
Map<String, Object> result = createSession(shell != null ? shell : "bash", publicKey, secretKey, opts).get();
System.out.println("Session created:");
printMap(result);
}
}
private static void handleService(
List<String> args,
String publicKey,
String secretKey,
String networkMode,
int vcpu,
List<String> envVars
) throws Exception {
// Check for "env" subcommand
if (args.size() > 1 && args.get(1).equals("env")) {
handleServiceEnv(args, publicKey, secretKey);
return;
}
// Parse service-specific options
boolean list = false;
String name = null;
String ports = null;
String bootstrap = null;
String infoId = null;
String logsId = null;
String freezeId = null;
String unfreezeId = null;
String destroyId = null;
String lockId = null;
String unlockId = null;
String executeId = null;
String executeCmd = null;
String redeployId = null;
String snapshotId = null;
int i = 1; // Skip "service" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
name = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
ports = args.get(++i);
i++;
} else if (arg.equals("--bootstrap")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --bootstrap requires a value");
System.exit(2);
}
bootstrap = args.get(++i);
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--logs")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --logs requires an ID");
System.exit(2);
}
logsId = args.get(++i);
i++;
} else if (arg.equals("--freeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --freeze requires an ID");
System.exit(2);
}
freezeId = args.get(++i);
i++;
} else if (arg.equals("--unfreeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unfreeze requires an ID");
System.exit(2);
}
unfreezeId = args.get(++i);
i++;
} else if (arg.equals("--destroy")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --destroy requires an ID");
System.exit(2);
}
destroyId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--execute")) {
if (i + 2 >= args.size()) {
System.err.println("Error: --execute requires ID and command");
System.exit(2);
}
executeId = args.get(++i);
executeCmd = args.get(++i);
i++;
} else if (arg.equals("--redeploy")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --redeploy requires an ID");
System.exit(2);
}
redeployId = args.get(++i);
i++;
} else if (arg.equals("--snapshot")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot requires an ID");
System.exit(2);
}
snapshotId = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> services = listServices(publicKey, secretKey).get();
printServiceList(services);
} else if (infoId != null) {
Map<String, Object> service = getService(infoId, publicKey, secretKey).get();
printMap(service);
} else if (logsId != null) {
Map<String, Object> logs = getServiceLogs(logsId, true, publicKey, secretKey).get();
Object content = logs.get("logs");
if (content != null) {
System.out.println(content);
}
} else if (freezeId != null) {
freezeService(freezeId, publicKey, secretKey).get();
System.out.println("Service frozen: " + freezeId);
} else if (unfreezeId != null) {
unfreezeService(unfreezeId, publicKey, secretKey).get();
System.out.println("Service unfrozen: " + unfreezeId);
} else if (destroyId != null) {
deleteService(destroyId, publicKey, secretKey).get();
System.out.println("Service destroyed: " + destroyId);
} else if (lockId != null) {
lockService(lockId, publicKey, secretKey).get();
System.out.println("Service locked: " + lockId);
} else if (unlockId != null) {
unlockService(unlockId, publicKey, secretKey).get();
System.out.println("Service unlocked: " + unlockId);
} else if (executeId != null && executeCmd != null) {
Map<String, Object> result = executeInService(executeId, executeCmd, publicKey, secretKey).get();
Object stdout = result.get("stdout");
if (stdout != null) {
System.out.print(stdout);
}
Object stderr = result.get("stderr");
if (stderr != null && !stderr.toString().isEmpty()) {
System.err.print(stderr);
}
} else if (redeployId != null) {
redeployService(redeployId, publicKey, secretKey).get();
System.out.println("Service redeployed: " + redeployId);
} else if (snapshotId != null) {
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null).get();
System.out.println("Snapshot created: " + snapId);
} else if (name != null) {
// Create new service
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey).get();
System.out.println("Service created:");
printMap(result);
} else {
System.err.println("Error: No service action specified. Use --list, --name, --info, etc.");
System.exit(2);
}
}
private static void handleServiceEnv(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
if (args.size() < 4) {
System.err.println("Error: service env requires action and service ID");
System.err.println("Usage: java UnsandboxAsync service env <status|set|export|delete> <service_id>");
System.exit(2);
}
String action = args.get(2);
String serviceId = args.get(3);
switch (action) {
case "status":
Map<String, Object> status = getServiceEnv(serviceId, publicKey, secretKey).get();
printMap(status);
break;
case "set":
// Read env from stdin
Map<String, String> env = new LinkedHashMap<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
int eqIdx = line.indexOf('=');
if (eqIdx > 0) {
String key = line.substring(0, eqIdx);
String value = line.substring(eqIdx + 1);
env.put(key, value);
}
}
}
if (!env.isEmpty()) {
setServiceEnv(serviceId, env, publicKey, secretKey).get();
System.out.println("Environment set for service: " + serviceId);
} else {
System.err.println("Error: No environment variables provided");
System.exit(1);
}
break;
case "export":
Map<String, Object> exported = exportServiceEnv(serviceId, publicKey, secretKey).get();
Object envData = exported.get("env");
if (envData instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> envMap = (Map<String, Object>) envData;
for (Map.Entry<String, Object> entry : envMap.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
break;
case "delete":
deleteServiceEnv(serviceId, null, publicKey, secretKey).get();
System.out.println("Environment deleted for service: " + serviceId);
break;
default:
System.err.println("Error: Unknown env action: " + action);
System.err.println("Valid actions: status, set, export, delete");
System.exit(2);
}
}
private static void handleSnapshot(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
// Parse snapshot-specific options
boolean list = false;
String infoId = null;
String deleteId = null;
String lockId = null;
String unlockId = null;
String cloneId = null;
String cloneName = null;
String cloneType = null;
String clonePorts = null;
int i = 1; // Skip "snapshot" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--delete")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --delete requires an ID");
System.exit(2);
}
deleteId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--clone")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --clone requires an ID");
System.exit(2);
}
cloneId = args.get(++i);
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
cloneName = args.get(++i);
i++;
} else if (arg.equals("--type")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --type requires a value");
System.exit(2);
}
cloneType = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
clonePorts = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> snapshots = listSnapshots(publicKey, secretKey).get();
printSnapshotList(snapshots);
} else if (infoId != null) {
// Get snapshot info via list and filter
List<Map<String, Object>> snapshots = listSnapshots(publicKey, secretKey).get();
for (Map<String, Object> snap : snapshots) {
Object id = snap.get("id");
if (id != null && id.toString().equals(infoId)) {
printMap(snap);
return;
}
}
System.err.println("Error: Snapshot not found: " + infoId);
System.exit(1);
} else if (deleteId != null) {
deleteSnapshot(deleteId, publicKey, secretKey).get();
System.out.println("Snapshot deleted: " + deleteId);
} else if (lockId != null) {
lockSnapshot(lockId, publicKey, secretKey).get();
System.out.println("Snapshot locked: " + lockId);
} else if (unlockId != null) {
unlockSnapshot(unlockId, publicKey, secretKey).get();
System.out.println("Snapshot unlocked: " + unlockId);
} else if (cloneId != null) {
Map<String, Object> result = cloneSnapshot(cloneId, cloneName, publicKey, secretKey).get();
System.out.println("Snapshot cloned:");
printMap(result);
} else {
System.err.println("Error: No snapshot action specified. Use --list, --info, --delete, etc.");
System.exit(2);
}
}
private static void handleKey(String publicKey, String secretKey) throws Exception {
Map<String, Object> result = validateKeys(publicKey, secretKey).get();
printMap(result);
}
private static void printSessionList(List<Map<String, Object>> sessions) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> session : sessions) {
String id = getStr(session, "id", "session_id");
String name = getStr(session, "name", "container_name");
String status = getStr(session, "status", "state");
String created = getStr(session, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created);
}
}
private static void printServiceList(List<Map<String, Object>> services) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> service : services) {
String id = getStr(service, "id", "service_id");
String name = getStr(service, "name", "");
String status = getStr(service, "state", "status");
String created = getStr(service, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created);
}
}
private static void printSnapshotList(List<Map<String, Object>> snapshots) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "TYPE", "CREATED");
for (Map<String, Object> snapshot : snapshots) {
String id = getStr(snapshot, "id", "snapshot_id");
String name = getStr(snapshot, "name", "");
String type = getStr(snapshot, "type", "source_type");
String created = getStr(snapshot, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, type, created);
}
}
private static String getStr(Map<String, Object> map, String key1, String key2) {
Object val = map.get(key1);
if (val != null) {
return val.toString();
}
val = map.get(key2);
if (val != null) {
return val.toString();
}
return "";
}
private static void printMap(Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}

View file

@ -1740,6 +1740,52 @@ public class Un {
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
}
// ========================================================================
// Image Generation API
// ========================================================================
/**
* Generate images from text prompt using AI.
*
* @param prompt Text description of the image to generate
* @param model Model to use (optional, can be null)
* @param size Image size (e.g., "1024x1024")
* @param quality "standard" or "hd"
* @param n Number of images to generate
* @param publicKey API public key (optional)
* @param secretKey API secret key (optional)
* @return Map containing "images" (List of base64/URLs) and "created_at"
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> image(
String prompt,
String model,
String size,
String quality,
int n,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
if (size == null) size = "1024x1024";
if (quality == null) quality = "standard";
if (n <= 0) n = 1;
Map<String, Object> data = new LinkedHashMap<>();
data.put("prompt", prompt);
data.put("size", size);
data.put("quality", quality);
data.put("n", n);
if (model != null && !model.isEmpty()) {
data.put("model", model);
}
return makeRequest("POST", "/image", creds[0], creds[1], data);
}
// ========================================================================
// HTTP Request Helpers
// ========================================================================
@ -1807,4 +1853,836 @@ public class Un {
return parseJson(responseBody);
}
// ========================================================================
// CLI Implementation
// ========================================================================
/**
* CLI entry point.
*/
public static void main(String[] args) {
try {
cliMain(args);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
}
/**
* CLI main implementation.
*/
public static void cliMain(String[] args) throws Exception {
if (args.length == 0) {
printHelp();
System.exit(0);
}
// Parse global options
String publicKey = null;
String secretKey = null;
String language = null;
String networkMode = "zerotrust";
int vcpu = 1;
List<String> envVars = new ArrayList<>();
List<String> files = new ArrayList<>();
List<String> positionalArgs = new ArrayList<>();
boolean showHelp = false;
int i = 0;
while (i < args.length) {
String arg = args[i];
if (arg.equals("-h") || arg.equals("--help")) {
showHelp = true;
i++;
} else if (arg.equals("-s") || arg.equals("--shell")) {
if (i + 1 >= args.length) {
System.err.println("Error: -s/--shell requires an argument");
System.exit(2);
}
language = args[++i];
i++;
} else if (arg.equals("-p") || arg.equals("--public-key")) {
if (i + 1 >= args.length) {
System.err.println("Error: -p/--public-key requires an argument");
System.exit(2);
}
publicKey = args[++i];
i++;
} else if (arg.equals("-k") || arg.equals("--secret-key")) {
if (i + 1 >= args.length) {
System.err.println("Error: -k/--secret-key requires an argument");
System.exit(2);
}
secretKey = args[++i];
i++;
} else if (arg.equals("-n") || arg.equals("--network")) {
if (i + 1 >= args.length) {
System.err.println("Error: -n/--network requires an argument");
System.exit(2);
}
networkMode = args[++i];
i++;
} else if (arg.equals("-v") || arg.equals("--vcpu")) {
if (i + 1 >= args.length) {
System.err.println("Error: -v/--vcpu requires an argument");
System.exit(2);
}
vcpu = Integer.parseInt(args[++i]);
i++;
} else if (arg.equals("-e") || arg.equals("--env")) {
if (i + 1 >= args.length) {
System.err.println("Error: -e/--env requires an argument");
System.exit(2);
}
envVars.add(args[++i]);
i++;
} else if (arg.equals("-f") || arg.equals("--file")) {
if (i + 1 >= args.length) {
System.err.println("Error: -f/--file requires an argument");
System.exit(2);
}
files.add(args[++i]);
i++;
} else if (arg.startsWith("-")) {
System.err.println("Error: Unknown option: " + arg);
System.exit(2);
} else {
positionalArgs.add(arg);
i++;
}
}
if (showHelp || positionalArgs.isEmpty()) {
printHelp();
System.exit(0);
}
String command = positionalArgs.get(0);
// Route to subcommand handlers
switch (command) {
case "session":
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
break;
case "service":
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
break;
case "snapshot":
handleSnapshot(positionalArgs, publicKey, secretKey);
break;
case "key":
handleKey(publicKey, secretKey);
break;
default:
// Default: execute code
handleExecute(positionalArgs, publicKey, secretKey, language, networkMode, vcpu, envVars, files);
break;
}
}
private static void printHelp() {
System.out.println("Un - unsandbox.com CLI (Java Synchronous SDK)");
System.out.println();
System.out.println("Usage:");
System.out.println(" java Un [options] <source_file> Execute code file");
System.out.println(" java Un [options] -s LANG 'code' Execute inline code");
System.out.println(" java Un session [options] Interactive session");
System.out.println(" java Un service [options] Manage services");
System.out.println(" java Un snapshot [options] Manage snapshots");
System.out.println(" java Un key Check API key");
System.out.println();
System.out.println("Global Options:");
System.out.println(" -s, --shell LANG Language for inline code");
System.out.println(" -e, --env KEY=VAL Set environment variable");
System.out.println(" -f, --file FILE Add input file to /tmp/");
System.out.println(" -p, --public-key KEY API public key");
System.out.println(" -k, --secret-key KEY API secret key");
System.out.println(" -n, --network MODE Network: zerotrust or semitrusted");
System.out.println(" -v, --vcpu N vCPU count (1-8)");
System.out.println(" -h, --help Show help");
System.out.println();
System.out.println("Session Options:");
System.out.println(" --list, -l List active sessions");
System.out.println(" --attach ID Reconnect to session");
System.out.println(" --kill ID Terminate session");
System.out.println(" --freeze ID Pause session");
System.out.println(" --unfreeze ID Resume session");
System.out.println(" --boost ID Add resources");
System.out.println(" --unboost ID Remove boost");
System.out.println(" --snapshot ID Create snapshot");
System.out.println(" --tmux Enable persistence with tmux");
System.out.println(" --screen Enable persistence with screen");
System.out.println(" --shell SHELL Shell/REPL to use");
System.out.println();
System.out.println("Service Options:");
System.out.println(" --list, -l List all services");
System.out.println(" --name NAME Service name (creates new)");
System.out.println(" --ports PORTS Comma-separated ports");
System.out.println(" --bootstrap CMD Bootstrap command");
System.out.println(" --info ID Get service details");
System.out.println(" --logs ID Get all logs");
System.out.println(" --freeze ID Pause service");
System.out.println(" --unfreeze ID Resume service");
System.out.println(" --destroy ID Delete service");
System.out.println(" --lock ID Prevent deletion");
System.out.println(" --unlock ID Allow deletion");
System.out.println(" --execute ID CMD Run command in service");
System.out.println(" --redeploy ID Re-run bootstrap");
System.out.println(" --snapshot ID Create snapshot");
System.out.println();
System.out.println("Service Env Subcommand:");
System.out.println(" java Un service env status ID Show vault status");
System.out.println(" java Un service env set ID Set from stdin");
System.out.println(" java Un service env export ID Export to stdout");
System.out.println(" java Un service env delete ID Delete vault");
System.out.println();
System.out.println("Snapshot Options:");
System.out.println(" --list, -l List all snapshots");
System.out.println(" --info ID Get snapshot details");
System.out.println(" --delete ID Delete snapshot");
System.out.println(" --lock ID Prevent deletion");
System.out.println(" --unlock ID Allow deletion");
System.out.println(" --clone ID Clone snapshot");
}
private static void handleExecute(
List<String> args,
String publicKey,
String secretKey,
String language,
String networkMode,
int vcpu,
List<String> envVars,
List<String> files
) throws Exception {
String codeOrFile = args.get(0);
String code;
if (language != null) {
// Inline code mode: -s python 'print(1)'
code = codeOrFile;
} else {
// File mode: script.py
Path filePath = Paths.get(codeOrFile);
if (!Files.exists(filePath)) {
System.err.println("Error: File not found: " + codeOrFile);
System.exit(1);
}
code = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
language = detectLanguage(codeOrFile);
if (language == null) {
System.err.println("Error: Cannot detect language from file extension: " + codeOrFile);
System.exit(1);
}
}
Map<String, Object> result = executeCode(language, code, publicKey, secretKey);
// Print output
Object stdout = result.get("stdout");
if (stdout != null && !stdout.toString().isEmpty()) {
System.out.print(stdout);
}
Object stderr = result.get("stderr");
if (stderr != null && !stderr.toString().isEmpty()) {
System.err.print(stderr);
}
System.out.println("---");
Object exitCode = result.get("exit_code");
System.out.println("Exit code: " + (exitCode != null ? exitCode : "0"));
Object executionTime = result.get("execution_time_ms");
if (executionTime != null) {
System.out.println("Execution time: " + executionTime + "ms");
}
// Exit with the code's exit code
if (exitCode != null && exitCode instanceof Number) {
int code_exit = ((Number) exitCode).intValue();
if (code_exit != 0) {
System.exit(code_exit);
}
}
}
private static void handleSession(
List<String> args,
String publicKey,
String secretKey,
String networkMode,
int vcpu,
String shell
) throws Exception {
// Parse session-specific options
boolean list = false;
String attachId = null;
String killId = null;
String freezeId = null;
String unfreezeId = null;
String boostId = null;
String unboostId = null;
String snapshotId = null;
String snapshotName = null;
boolean hot = false;
boolean useTmux = false;
boolean useScreen = false;
int i = 1; // Skip "session" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--attach")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --attach requires an ID");
System.exit(2);
}
attachId = args.get(++i);
i++;
} else if (arg.equals("--kill")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --kill requires an ID");
System.exit(2);
}
killId = args.get(++i);
i++;
} else if (arg.equals("--freeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --freeze requires an ID");
System.exit(2);
}
freezeId = args.get(++i);
i++;
} else if (arg.equals("--unfreeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unfreeze requires an ID");
System.exit(2);
}
unfreezeId = args.get(++i);
i++;
} else if (arg.equals("--boost")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --boost requires an ID");
System.exit(2);
}
boostId = args.get(++i);
i++;
} else if (arg.equals("--unboost")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unboost requires an ID");
System.exit(2);
}
unboostId = args.get(++i);
i++;
} else if (arg.equals("--snapshot")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot requires an ID");
System.exit(2);
}
snapshotId = args.get(++i);
i++;
} else if (arg.equals("--snapshot-name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot-name requires a name");
System.exit(2);
}
snapshotName = args.get(++i);
i++;
} else if (arg.equals("--hot")) {
hot = true;
i++;
} else if (arg.equals("--tmux")) {
useTmux = true;
i++;
} else if (arg.equals("--screen")) {
useScreen = true;
i++;
} else if (arg.equals("--shell")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --shell requires a value");
System.exit(2);
}
shell = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> sessions = listSessions(publicKey, secretKey);
printSessionList(sessions);
} else if (attachId != null) {
Map<String, Object> session = getSession(attachId, publicKey, secretKey);
System.out.println("Session: " + attachId);
printMap(session);
} else if (killId != null) {
deleteSession(killId, publicKey, secretKey);
System.out.println("Session terminated: " + killId);
} else if (freezeId != null) {
freezeSession(freezeId, publicKey, secretKey);
System.out.println("Session frozen: " + freezeId);
} else if (unfreezeId != null) {
unfreezeSession(unfreezeId, publicKey, secretKey);
System.out.println("Session unfrozen: " + unfreezeId);
} else if (boostId != null) {
boostSession(boostId, publicKey, secretKey);
System.out.println("Session boosted: " + boostId);
} else if (unboostId != null) {
unboostSession(unboostId, publicKey, secretKey);
System.out.println("Session unboosted: " + unboostId);
} else if (snapshotId != null) {
String snapId = sessionSnapshot(snapshotId, publicKey, secretKey, snapshotName, hot);
System.out.println("Snapshot created: " + snapId);
} else {
// Create new session
Map<String, Object> opts = new LinkedHashMap<>();
opts.put("network_mode", networkMode);
if (vcpu > 1) {
opts.put("vcpu", vcpu);
}
if (useTmux) {
opts.put("multiplexer", "tmux");
} else if (useScreen) {
opts.put("multiplexer", "screen");
}
Map<String, Object> result = createSession(shell != null ? shell : "bash", publicKey, secretKey, opts);
System.out.println("Session created:");
printMap(result);
}
}
private static void handleService(
List<String> args,
String publicKey,
String secretKey,
String networkMode,
int vcpu,
List<String> envVars
) throws Exception {
// Check for "env" subcommand
if (args.size() > 1 && args.get(1).equals("env")) {
handleServiceEnv(args, publicKey, secretKey);
return;
}
// Parse service-specific options
boolean list = false;
String name = null;
String ports = null;
String bootstrap = null;
String infoId = null;
String logsId = null;
String freezeId = null;
String unfreezeId = null;
String destroyId = null;
String lockId = null;
String unlockId = null;
String executeId = null;
String executeCmd = null;
String redeployId = null;
String snapshotId = null;
int i = 1; // Skip "service" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
name = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
ports = args.get(++i);
i++;
} else if (arg.equals("--bootstrap")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --bootstrap requires a value");
System.exit(2);
}
bootstrap = args.get(++i);
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--logs")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --logs requires an ID");
System.exit(2);
}
logsId = args.get(++i);
i++;
} else if (arg.equals("--freeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --freeze requires an ID");
System.exit(2);
}
freezeId = args.get(++i);
i++;
} else if (arg.equals("--unfreeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unfreeze requires an ID");
System.exit(2);
}
unfreezeId = args.get(++i);
i++;
} else if (arg.equals("--destroy")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --destroy requires an ID");
System.exit(2);
}
destroyId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--execute")) {
if (i + 2 >= args.size()) {
System.err.println("Error: --execute requires ID and command");
System.exit(2);
}
executeId = args.get(++i);
executeCmd = args.get(++i);
i++;
} else if (arg.equals("--redeploy")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --redeploy requires an ID");
System.exit(2);
}
redeployId = args.get(++i);
i++;
} else if (arg.equals("--snapshot")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot requires an ID");
System.exit(2);
}
snapshotId = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> services = listServices(publicKey, secretKey);
printServiceList(services);
} else if (infoId != null) {
Map<String, Object> service = getService(infoId, publicKey, secretKey);
printMap(service);
} else if (logsId != null) {
Map<String, Object> logs = getServiceLogs(logsId, true, publicKey, secretKey);
Object content = logs.get("logs");
if (content != null) {
System.out.println(content);
}
} else if (freezeId != null) {
freezeService(freezeId, publicKey, secretKey);
System.out.println("Service frozen: " + freezeId);
} else if (unfreezeId != null) {
unfreezeService(unfreezeId, publicKey, secretKey);
System.out.println("Service unfrozen: " + unfreezeId);
} else if (destroyId != null) {
deleteService(destroyId, publicKey, secretKey);
System.out.println("Service destroyed: " + destroyId);
} else if (lockId != null) {
lockService(lockId, publicKey, secretKey);
System.out.println("Service locked: " + lockId);
} else if (unlockId != null) {
unlockService(unlockId, publicKey, secretKey);
System.out.println("Service unlocked: " + unlockId);
} else if (executeId != null && executeCmd != null) {
Map<String, Object> result = executeInService(executeId, executeCmd, publicKey, secretKey);
Object stdout = result.get("stdout");
if (stdout != null) {
System.out.print(stdout);
}
Object stderr = result.get("stderr");
if (stderr != null && !stderr.toString().isEmpty()) {
System.err.print(stderr);
}
} else if (redeployId != null) {
redeployService(redeployId, publicKey, secretKey);
System.out.println("Service redeployed: " + redeployId);
} else if (snapshotId != null) {
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
System.out.println("Snapshot created: " + snapId);
} else if (name != null) {
// Create new service
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey);
System.out.println("Service created:");
printMap(result);
} else {
System.err.println("Error: No service action specified. Use --list, --name, --info, etc.");
System.exit(2);
}
}
private static void handleServiceEnv(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
if (args.size() < 4) {
System.err.println("Error: service env requires action and service ID");
System.err.println("Usage: java Un service env <status|set|export|delete> <service_id>");
System.exit(2);
}
String action = args.get(2);
String serviceId = args.get(3);
switch (action) {
case "status":
Map<String, Object> status = getServiceEnv(serviceId, publicKey, secretKey);
printMap(status);
break;
case "set":
// Read env from stdin
Map<String, String> env = new LinkedHashMap<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
int eqIdx = line.indexOf('=');
if (eqIdx > 0) {
String key = line.substring(0, eqIdx);
String value = line.substring(eqIdx + 1);
env.put(key, value);
}
}
}
if (!env.isEmpty()) {
setServiceEnv(serviceId, env, publicKey, secretKey);
System.out.println("Environment set for service: " + serviceId);
} else {
System.err.println("Error: No environment variables provided");
System.exit(1);
}
break;
case "export":
Map<String, Object> exported = exportServiceEnv(serviceId, publicKey, secretKey);
Object envData = exported.get("env");
if (envData instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> envMap = (Map<String, Object>) envData;
for (Map.Entry<String, Object> entry : envMap.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
break;
case "delete":
deleteServiceEnv(serviceId, null, publicKey, secretKey);
System.out.println("Environment deleted for service: " + serviceId);
break;
default:
System.err.println("Error: Unknown env action: " + action);
System.err.println("Valid actions: status, set, export, delete");
System.exit(2);
}
}
private static void handleSnapshot(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
// Parse snapshot-specific options
boolean list = false;
String infoId = null;
String deleteId = null;
String lockId = null;
String unlockId = null;
String cloneId = null;
String cloneName = null;
String cloneType = null;
String clonePorts = null;
int i = 1; // Skip "snapshot" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--delete")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --delete requires an ID");
System.exit(2);
}
deleteId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--clone")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --clone requires an ID");
System.exit(2);
}
cloneId = args.get(++i);
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
cloneName = args.get(++i);
i++;
} else if (arg.equals("--type")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --type requires a value");
System.exit(2);
}
cloneType = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
clonePorts = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> snapshots = listSnapshots(publicKey, secretKey);
printSnapshotList(snapshots);
} else if (infoId != null) {
// Get snapshot info via restore API (or just list and filter)
List<Map<String, Object>> snapshots = listSnapshots(publicKey, secretKey);
for (Map<String, Object> snap : snapshots) {
Object id = snap.get("id");
if (id != null && id.toString().equals(infoId)) {
printMap(snap);
return;
}
}
System.err.println("Error: Snapshot not found: " + infoId);
System.exit(1);
} else if (deleteId != null) {
deleteSnapshot(deleteId, publicKey, secretKey);
System.out.println("Snapshot deleted: " + deleteId);
} else if (lockId != null) {
lockSnapshot(lockId, publicKey, secretKey);
System.out.println("Snapshot locked: " + lockId);
} else if (unlockId != null) {
unlockSnapshot(unlockId, publicKey, secretKey);
System.out.println("Snapshot unlocked: " + unlockId);
} else if (cloneId != null) {
Map<String, Object> result = cloneSnapshot(cloneId, cloneName, publicKey, secretKey);
System.out.println("Snapshot cloned:");
printMap(result);
} else {
System.err.println("Error: No snapshot action specified. Use --list, --info, --delete, etc.");
System.exit(2);
}
}
private static void handleKey(String publicKey, String secretKey) throws Exception {
Map<String, Object> result = validateKeys(publicKey, secretKey);
printMap(result);
}
private static void printSessionList(List<Map<String, Object>> sessions) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> session : sessions) {
String id = getStr(session, "id", "session_id");
String name = getStr(session, "name", "container_name");
String status = getStr(session, "status", "state");
String created = getStr(session, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created);
}
}
private static void printServiceList(List<Map<String, Object>> services) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> service : services) {
String id = getStr(service, "id", "service_id");
String name = getStr(service, "name", "");
String status = getStr(service, "state", "status");
String created = getStr(service, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created);
}
}
private static void printSnapshotList(List<Map<String, Object>> snapshots) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "TYPE", "CREATED");
for (Map<String, Object> snapshot : snapshots) {
String id = getStr(snapshot, "id", "snapshot_id");
String name = getStr(snapshot, "name", "");
String type = getStr(snapshot, "type", "source_type");
String created = getStr(snapshot, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, type, created);
}
}
private static String getStr(Map<String, Object> map, String key1, String key2) {
Object val = map.get(key1);
if (val != null) {
return val.toString();
}
val = map.get(key2);
if (val != null) {
return val.toString();
}
return "";
}
private static void printMap(Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}

View file

@ -1067,6 +1067,35 @@ async function validateKeys(publicKey, secretKey) {
return makeRequest('POST', '/keys/validate', publicKey, secretKey, {});
}
// ============================================================================
// Image Generation
// ============================================================================
/**
* Generate images from text prompt using AI.
*
* Args:
* prompt: Text description of the image to generate
* options: Generation options:
* - model: Model to use (optional)
* - size: Image size (default: "1024x1024")
* - quality: "standard" or "hd" (default: "standard")
* - n: Number of images (default: 1)
* - publicKey: API public key
* - secretKey: API secret key
*
* Returns: Promise<Object> (result with images array and created_at)
*/
async function image(prompt, options = {}) {
const { model, size = "1024x1024", quality = "standard", n = 1, publicKey, secretKey } = options;
const [pk, sk] = resolveCredentials(publicKey, secretKey);
const payload = { prompt, size, quality, n };
if (model) payload.model = model;
return makeRequest('POST', '/image', pk, sk, payload);
}
// ES Module exports
export {
// Code execution
@ -1116,9 +1145,13 @@ export {
cloneSnapshot,
// Key validation
validateKeys,
// Image generation
image,
// Errors
CredentialsError,
TimeoutError,
// CLI
cliMain,
};
// Default export for convenience
@ -1170,7 +1203,929 @@ export default {
cloneSnapshot,
// Key validation
validateKeys,
// Image generation
image,
// Errors
CredentialsError,
TimeoutError,
// CLI
cliMain,
};
// ============================================================================
// CLI Implementation
// ============================================================================
const HELP_TEXT = `
unsandbox CLI - Secure code execution platform (async version)
USAGE:
node un_async.js [options] <source_file> Execute code file
node un_async.js -s <lang> '<code>' Execute inline code
node un_async.js session [options] Interactive session management
node un_async.js service [options] Service management
node un_async.js snapshot [options] Snapshot management
node un_async.js key Check API key validity
GLOBAL OPTIONS:
-s, --shell <lang> Language for inline code execution
-e, --env <KEY=VAL> Set environment variable (can be repeated)
-f, --file <path> Add input file to /tmp/ (can be repeated)
-F, --file-path <path> Add input file with path preserved
-a, --artifacts Return compiled artifacts
-o, --output <dir> Output directory for artifacts
-p, --public-key <key> API public key
-k, --secret-key <key> API secret key
-n, --network <mode> Network mode: zerotrust (default) or semitrusted
-v, --vcpu <n> vCPU count (1-8)
-y, --yes Skip confirmation prompts
-h, --help Show this help message
SESSION COMMANDS:
node un_async.js session Start interactive bash session
node un_async.js session --shell python3 Start Python REPL
node un_async.js session --tmux Persistent session with tmux
node un_async.js session --screen Persistent session with screen
node un_async.js session --list List active sessions
node un_async.js session --attach <id> Reconnect to session
node un_async.js session --kill <id> Terminate session
node un_async.js session --freeze <id> Pause session
node un_async.js session --unfreeze <id> Resume session
node un_async.js session --boost <id> Add resources
node un_async.js session --unboost <id> Remove boost
node un_async.js session --snapshot <id> Create snapshot
SERVICE COMMANDS:
node un_async.js service --list List all services
node un_async.js service --name <n> --ports <p> --bootstrap <cmd>
Create new service
node un_async.js service --info <id> Get service details
node un_async.js service --logs <id> Get service logs
node un_async.js service --tail <id> Get last 9000 lines
node un_async.js service --freeze <id> Pause service
node un_async.js service --unfreeze <id> Resume service
node un_async.js service --destroy <id> Delete service
node un_async.js service --lock <id> Prevent deletion
node un_async.js service --unlock <id> Allow deletion
node un_async.js service --execute <id> <cmd> Run command in service
node un_async.js service --redeploy <id> Re-run bootstrap
node un_async.js service --snapshot <id> Create snapshot
SERVICE ENV COMMANDS:
node un_async.js service env status <id> Show vault status
node un_async.js service env set <id> Set from --env-file or stdin
node un_async.js service env export <id> Export to stdout
node un_async.js service env delete <id> Delete vault
SNAPSHOT COMMANDS:
node un_async.js snapshot --list List all snapshots
node un_async.js snapshot --info <id> Get snapshot details
node un_async.js snapshot --delete <id> Delete snapshot
node un_async.js snapshot --lock <id> Prevent deletion
node un_async.js snapshot --unlock <id> Allow deletion
node un_async.js snapshot --clone <id> Clone snapshot to new resource
EXAMPLES:
node un_async.js script.py Execute Python script
node un_async.js -s bash 'echo hello' Run bash command
node un_async.js -e DEBUG=1 script.py Execute with env var
node un_async.js -n semitrusted crawler.py Execute with network access
node un_async.js session --tmux Start persistent session
node un_async.js service --list List all services
`;
/**
* Parse command line arguments manually.
* Returns object with parsed options and positional args.
*/
function parseArgs(args) {
const result = {
command: null, // session, service, snapshot, key, or null (execute)
subcommand: null, // env (for service env commands)
positional: [],
shell: null,
env: [],
files: [],
filesWithPath: [],
artifacts: false,
output: null,
publicKey: null,
secretKey: null,
network: 'zerotrust',
vcpu: 1,
yes: false,
help: false,
// Session options
list: false,
attach: null,
kill: null,
freeze: null,
unfreeze: null,
boost: null,
unboost: null,
snapshot: null,
snapshotName: null,
hot: false,
audit: false,
tmux: false,
screen: false,
// Service options
name: null,
ports: null,
domains: null,
type: null,
bootstrap: null,
bootstrapFile: null,
envFile: null,
info: null,
logs: null,
tail: null,
destroy: null,
lock: null,
unlock: null,
resize: null,
redeploy: null,
execute: null,
executeCmd: null,
// Snapshot options
delete: null,
clone: null,
};
let i = 0;
while (i < args.length) {
const arg = args[i];
// Check for subcommands first
if (arg === 'session' && result.command === null) {
result.command = 'session';
i++;
continue;
}
if (arg === 'service' && result.command === null) {
result.command = 'service';
i++;
continue;
}
if (arg === 'snapshot' && result.command === null) {
result.command = 'snapshot';
i++;
continue;
}
if (arg === 'key' && result.command === null) {
result.command = 'key';
i++;
continue;
}
// Service env subcommand
if (arg === 'env' && result.command === 'service') {
result.subcommand = 'env';
i++;
continue;
}
// Service env operations (status, set, export, delete)
if (result.command === 'service' && result.subcommand === 'env') {
if (['status', 'set', 'export', 'delete'].includes(arg)) {
result.envOperation = arg;
i++;
continue;
}
}
// Parse options
if (arg === '-h' || arg === '--help') {
result.help = true;
i++;
} else if (arg === '-s' || arg === '--shell') {
result.shell = args[++i];
i++;
} else if (arg === '-e' || arg === '--env') {
result.env.push(args[++i]);
i++;
} else if (arg === '-f' || arg === '--file') {
result.files.push(args[++i]);
i++;
} else if (arg === '-F' || arg === '--file-path') {
result.filesWithPath.push(args[++i]);
i++;
} else if (arg === '-a' || arg === '--artifacts') {
result.artifacts = true;
i++;
} else if (arg === '-o' || arg === '--output') {
result.output = args[++i];
i++;
} else if (arg === '-p' || arg === '--public-key') {
result.publicKey = args[++i];
i++;
} else if (arg === '-k' || arg === '--secret-key') {
result.secretKey = args[++i];
i++;
} else if (arg === '-n' || arg === '--network') {
result.network = args[++i];
i++;
} else if (arg === '-v' || arg === '--vcpu') {
result.vcpu = parseInt(args[++i], 10);
i++;
} else if (arg === '-y' || arg === '--yes') {
result.yes = true;
i++;
} else if (arg === '-l' || arg === '--list') {
result.list = true;
i++;
} else if (arg === '--attach') {
result.attach = args[++i];
i++;
} else if (arg === '--kill') {
result.kill = args[++i];
i++;
} else if (arg === '--freeze') {
result.freeze = args[++i];
i++;
} else if (arg === '--unfreeze') {
result.unfreeze = args[++i];
i++;
} else if (arg === '--boost') {
result.boost = args[++i];
i++;
} else if (arg === '--unboost') {
result.unboost = args[++i];
i++;
} else if (arg === '--snapshot') {
result.snapshot = args[++i];
i++;
} else if (arg === '--snapshot-name') {
result.snapshotName = args[++i];
i++;
} else if (arg === '--hot') {
result.hot = true;
i++;
} else if (arg === '--audit') {
result.audit = true;
i++;
} else if (arg === '--tmux') {
result.tmux = true;
i++;
} else if (arg === '--screen') {
result.screen = true;
i++;
} else if (arg === '--name') {
result.name = args[++i];
i++;
} else if (arg === '--ports') {
result.ports = args[++i];
i++;
} else if (arg === '--domains') {
result.domains = args[++i];
i++;
} else if (arg === '--type') {
result.type = args[++i];
i++;
} else if (arg === '--bootstrap') {
result.bootstrap = args[++i];
i++;
} else if (arg === '--bootstrap-file') {
result.bootstrapFile = args[++i];
i++;
} else if (arg === '--env-file') {
result.envFile = args[++i];
i++;
} else if (arg === '--info') {
result.info = args[++i];
i++;
} else if (arg === '--logs') {
result.logs = args[++i];
i++;
} else if (arg === '--tail') {
result.tail = args[++i];
i++;
} else if (arg === '--destroy') {
result.destroy = args[++i];
i++;
} else if (arg === '--lock') {
result.lock = args[++i];
i++;
} else if (arg === '--unlock') {
result.unlock = args[++i];
i++;
} else if (arg === '--resize') {
result.resize = args[++i];
i++;
} else if (arg === '--redeploy') {
result.redeploy = args[++i];
i++;
} else if (arg === '--execute') {
result.execute = args[++i];
// Next arg is the command to execute
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
result.executeCmd = args[++i];
}
i++;
} else if (arg === '--delete') {
result.delete = args[++i];
i++;
} else if (arg === '--clone') {
result.clone = args[++i];
i++;
} else if (arg.startsWith('-')) {
console.error(`Error: Unknown option: ${arg}`);
process.exit(2);
} else {
result.positional.push(arg);
i++;
}
}
return result;
}
/**
* Format timestamp for display.
*/
function formatTimestamp(ts) {
if (!ts) return 'N/A';
const date = new Date(ts * 1000);
return date.toISOString().replace('T', ' ').substring(0, 19);
}
/**
* Format list output in table format.
*/
function formatTable(items, columns) {
if (!items || items.length === 0) {
console.log('No items found.');
return;
}
// Calculate column widths
const widths = {};
for (const col of columns) {
widths[col.key] = col.label.length;
for (const item of items) {
const val = String(col.getter ? col.getter(item) : (item[col.key] || 'N/A'));
widths[col.key] = Math.max(widths[col.key], val.length);
}
}
// Print header
let header = '';
for (const col of columns) {
header += col.label.padEnd(widths[col.key] + 2);
}
console.log(header);
// Print rows
for (const item of items) {
let row = '';
for (const col of columns) {
const val = String(col.getter ? col.getter(item) : (item[col.key] || 'N/A'));
row += val.padEnd(widths[col.key] + 2);
}
console.log(row);
}
}
/**
* Handle session commands.
*/
async function handleSession(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
// List sessions
if (opts.list) {
const sessions = await listSessions(pk, sk);
formatTable(sessions, [
{ key: 'session_id', label: 'ID' },
{ key: 'name', label: 'NAME' },
{ key: 'status', label: 'STATUS' },
{ key: 'created_at', label: 'CREATED', getter: (s) => formatTimestamp(s.created_at) },
]);
return;
}
// Attach to session
if (opts.attach) {
const session = await getSession(opts.attach, pk, sk);
console.log(`Session ${opts.attach}:`);
console.log(JSON.stringify(session, null, 2));
console.log('\nNote: Interactive attach requires WebSocket connection (not supported in this CLI)');
return;
}
// Kill session
if (opts.kill) {
await deleteSession(opts.kill, pk, sk);
console.log(`Session ${opts.kill} terminated.`);
return;
}
// Freeze session
if (opts.freeze) {
await freezeSession(opts.freeze, pk, sk);
console.log(`Session ${opts.freeze} frozen.`);
return;
}
// Unfreeze session
if (opts.unfreeze) {
await unfreezeSession(opts.unfreeze, pk, sk);
console.log(`Session ${opts.unfreeze} unfrozen.`);
return;
}
// Boost session
if (opts.boost) {
await boostSession(opts.boost, opts.vcpu || 2, pk, sk);
console.log(`Session ${opts.boost} boosted.`);
return;
}
// Unboost session
if (opts.unboost) {
await unboostSession(opts.unboost, pk, sk);
console.log(`Session ${opts.unboost} unboosted.`);
return;
}
// Snapshot session
if (opts.snapshot) {
const snapshotId = await sessionSnapshot(opts.snapshot, pk, sk, opts.snapshotName, opts.hot);
console.log(`Snapshot created: ${snapshotId}`);
return;
}
// Create new session
const sessionOpts = {
networkMode: opts.network,
vcpu: opts.vcpu,
};
if (opts.tmux) sessionOpts.multiplexer = 'tmux';
if (opts.screen) sessionOpts.multiplexer = 'screen';
const session = await createSession(opts.shell || 'bash', sessionOpts, pk, sk);
console.log(`Session created: ${session.session_id}`);
console.log(JSON.stringify(session, null, 2));
console.log('\nNote: Interactive session requires WebSocket connection (not supported in this CLI)');
}
/**
* Handle service commands.
*/
async function handleService(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
// Handle env subcommand
if (opts.subcommand === 'env') {
const serviceId = opts.positional[0];
if (!serviceId) {
console.error('Error: Service ID required for env commands');
process.exit(2);
}
switch (opts.envOperation) {
case 'status': {
const status = await getServiceEnv(serviceId, pk, sk);
console.log(JSON.stringify(status, null, 2));
break;
}
case 'set': {
let envContent;
if (opts.envFile) {
envContent = fs.readFileSync(opts.envFile, 'utf-8');
} else {
// Read from stdin
envContent = fs.readFileSync(0, 'utf-8');
}
await setServiceEnv(serviceId, envContent, pk, sk);
console.log('Environment vault updated.');
break;
}
case 'export': {
const exported = await exportServiceEnv(serviceId, pk, sk);
if (exported.content) {
console.log(exported.content);
} else {
console.log(JSON.stringify(exported, null, 2));
}
break;
}
case 'delete': {
await deleteServiceEnv(serviceId, null, pk, sk);
console.log('Environment vault deleted.');
break;
}
default:
console.error('Error: Unknown env operation. Use: status, set, export, delete');
process.exit(2);
}
return;
}
// List services
if (opts.list) {
const services = await listServices(pk, sk);
formatTable(services, [
{ key: 'service_id', label: 'ID' },
{ key: 'name', label: 'NAME' },
{ key: 'status', label: 'STATUS' },
{ key: 'created_at', label: 'CREATED', getter: (s) => formatTimestamp(s.created_at) },
]);
return;
}
// Get service info
if (opts.info) {
const service = await getService(opts.info, pk, sk);
console.log(JSON.stringify(service, null, 2));
return;
}
// Get service logs
if (opts.logs) {
const logs = await getServiceLogs(opts.logs, true, pk, sk);
if (logs.logs) {
console.log(logs.logs);
} else if (logs.stdout) {
console.log(logs.stdout);
} else {
console.log(JSON.stringify(logs, null, 2));
}
return;
}
// Get service tail
if (opts.tail) {
const logs = await getServiceLogs(opts.tail, false, pk, sk);
if (logs.logs) {
console.log(logs.logs);
} else if (logs.stdout) {
console.log(logs.stdout);
} else {
console.log(JSON.stringify(logs, null, 2));
}
return;
}
// Freeze service
if (opts.freeze) {
await freezeService(opts.freeze, pk, sk);
console.log(`Service ${opts.freeze} frozen.`);
return;
}
// Unfreeze service
if (opts.unfreeze) {
await unfreezeService(opts.unfreeze, pk, sk);
console.log(`Service ${opts.unfreeze} unfrozen.`);
return;
}
// Destroy service
if (opts.destroy) {
await deleteService(opts.destroy, pk, sk);
console.log(`Service ${opts.destroy} destroyed.`);
return;
}
// Lock service
if (opts.lock) {
await lockService(opts.lock, pk, sk);
console.log(`Service ${opts.lock} locked.`);
return;
}
// Unlock service
if (opts.unlock) {
await unlockService(opts.unlock, pk, sk);
console.log(`Service ${opts.unlock} unlocked.`);
return;
}
// Resize service
if (opts.resize) {
await updateService(opts.resize, { vcpu: opts.vcpu }, pk, sk);
console.log(`Service ${opts.resize} resized.`);
return;
}
// Redeploy service
if (opts.redeploy) {
let bootstrap = opts.bootstrap;
if (opts.bootstrapFile) {
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
}
await redeployService(opts.redeploy, bootstrap, pk, sk);
console.log(`Service ${opts.redeploy} redeployed.`);
return;
}
// Execute command in service
if (opts.execute) {
if (!opts.executeCmd) {
console.error('Error: Command required for --execute');
process.exit(2);
}
const result = await executeInService(opts.execute, opts.executeCmd, 30000, pk, sk);
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.exit_code !== undefined) {
console.log('---');
console.log(`Exit code: ${result.exit_code}`);
}
return;
}
// Snapshot service
if (opts.snapshot) {
const snapshotId = await serviceSnapshot(opts.snapshot, pk, sk, opts.snapshotName);
console.log(`Snapshot created: ${snapshotId}`);
return;
}
// Create new service
if (opts.name) {
let ports = [];
if (opts.ports) {
ports = opts.ports.split(',').map((p) => parseInt(p.trim(), 10));
}
let bootstrap = opts.bootstrap;
if (opts.bootstrapFile) {
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
}
const serviceOpts = {
networkMode: opts.network,
vcpu: opts.vcpu,
};
if (opts.domains) {
serviceOpts.domains = opts.domains.split(',').map((d) => d.trim());
}
if (opts.type) {
serviceOpts.serviceType = opts.type;
}
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
console.log(`Service created: ${service.service_id}`);
console.log(JSON.stringify(service, null, 2));
return;
}
// No action specified
console.error('Error: No service action specified. Use --list, --name, --info, etc.');
process.exit(2);
}
/**
* Handle snapshot commands.
*/
async function handleSnapshot(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
// List snapshots
if (opts.list) {
const snapshots = await listSnapshots(pk, sk);
formatTable(snapshots, [
{ key: 'snapshot_id', label: 'ID' },
{ key: 'name', label: 'NAME' },
{ key: 'type', label: 'TYPE' },
{ key: 'created_at', label: 'CREATED', getter: (s) => formatTimestamp(s.created_at) },
]);
return;
}
// Get snapshot info
if (opts.info) {
// Use GET /snapshots/{id}
const [resolvedPk, resolvedSk] = resolveCredentials(pk, sk);
const snapshot = await makeRequest('GET', `/snapshots/${opts.info}`, resolvedPk, resolvedSk);
console.log(JSON.stringify(snapshot, null, 2));
return;
}
// Delete snapshot
if (opts.delete) {
await deleteSnapshot(opts.delete, pk, sk);
console.log(`Snapshot ${opts.delete} deleted.`);
return;
}
// Lock snapshot
if (opts.lock) {
await lockSnapshot(opts.lock, pk, sk);
console.log(`Snapshot ${opts.lock} locked.`);
return;
}
// Unlock snapshot
if (opts.unlock) {
await unlockSnapshot(opts.unlock, pk, sk);
console.log(`Snapshot ${opts.unlock} unlocked.`);
return;
}
// Clone snapshot
if (opts.clone) {
const cloneOpts = {};
if (opts.type) cloneOpts.type = opts.type;
if (opts.shell) cloneOpts.shell = opts.shell;
if (opts.ports) {
cloneOpts.ports = opts.ports.split(',').map((p) => parseInt(p.trim(), 10));
}
const result = await cloneSnapshot(opts.clone, opts.name, cloneOpts, pk, sk);
console.log('Snapshot cloned:');
console.log(JSON.stringify(result, null, 2));
return;
}
// No action specified
console.error('Error: No snapshot action specified. Use --list, --info, --delete, --clone, etc.');
process.exit(2);
}
/**
* Handle key command.
*/
async function handleKey(opts) {
try {
const result = await validateKeys(opts.publicKey, opts.secretKey);
console.log('API Key Status:');
console.log(JSON.stringify(result, null, 2));
} catch (err) {
// If validate endpoint doesn't exist, just show that credentials were resolved
const [pk] = resolveCredentials(opts.publicKey, opts.secretKey);
console.log(`Public Key: ${pk}`);
console.log('Key validation endpoint returned error - key may still be valid.');
}
}
/**
* Handle execute command (default).
*/
async function handleExecute(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
let code;
let language;
// Inline code with -s flag
if (opts.shell && opts.positional.length > 0) {
language = opts.shell;
code = opts.positional[0];
}
// File execution
else if (opts.positional.length > 0) {
const filePath = opts.positional[0];
language = detectLanguage(filePath);
if (!language) {
console.error(`Error: Could not detect language for file: ${filePath}`);
console.error('Use -s <language> to specify explicitly.');
process.exit(2);
}
code = fs.readFileSync(filePath, 'utf-8');
} else {
console.error('Error: No source file or inline code provided.');
console.error('Use: node un_async.js <file> or node un_async.js -s <lang> "<code>"');
process.exit(2);
}
// Build execution options
const execPayload = {
language,
code,
};
// Add environment variables
if (opts.env.length > 0) {
execPayload.env = {};
for (const e of opts.env) {
const idx = e.indexOf('=');
if (idx > 0) {
execPayload.env[e.substring(0, idx)] = e.substring(idx + 1);
}
}
}
// Add network mode if not default
if (opts.network !== 'zerotrust') {
execPayload.network_mode = opts.network;
}
// Execute code
const [resolvedPk, resolvedSk] = resolveCredentials(pk, sk);
const result = await makeRequest('POST', '/execute', resolvedPk, resolvedSk, execPayload);
// If job_id returned, wait for completion
let finalResult = result;
if (result.job_id && ['pending', 'running'].includes(result.status)) {
finalResult = await waitForJob(result.job_id, resolvedPk, resolvedSk);
}
// Output results
if (finalResult.stdout) {
process.stdout.write(finalResult.stdout);
}
if (finalResult.stderr) {
process.stderr.write(finalResult.stderr);
}
// Print summary
console.log('---');
if (finalResult.exit_code !== undefined) {
console.log(`Exit code: ${finalResult.exit_code}`);
}
if (finalResult.execution_time_ms !== undefined) {
console.log(`Execution time: ${finalResult.execution_time_ms}ms`);
} else if (finalResult.duration_ms !== undefined) {
console.log(`Execution time: ${finalResult.duration_ms}ms`);
}
// Exit with code's exit code
if (finalResult.exit_code && finalResult.exit_code !== 0) {
process.exit(1);
}
}
/**
* Main CLI entry point.
*/
async function cliMain() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(HELP_TEXT);
process.exit(0);
}
const opts = parseArgs(args);
if (opts.help) {
console.log(HELP_TEXT);
process.exit(0);
}
try {
switch (opts.command) {
case 'session':
await handleSession(opts);
break;
case 'service':
await handleService(opts);
break;
case 'snapshot':
await handleSnapshot(opts);
break;
case 'key':
await handleKey(opts);
break;
default:
await handleExecute(opts);
}
} catch (err) {
if (err instanceof CredentialsError) {
console.error(`Error: ${err.message}`);
process.exit(3);
} else if (err instanceof TimeoutError) {
console.error(`Error: ${err.message}`);
process.exit(5);
} else if (err.message && err.message.includes('HTTP 401')) {
console.error('Error: Authentication failed. Check your API keys.');
process.exit(3);
} else if (err.message && err.message.includes('HTTP 403')) {
console.error('Error: Access denied.');
process.exit(3);
} else if (err.message && err.message.includes('HTTP 404')) {
console.error('Error: Resource not found.');
process.exit(4);
} else if (err.message && err.message.includes('timeout')) {
console.error('Error: Request timeout.');
process.exit(5);
} else {
console.error(`Error: ${err.message}`);
process.exit(1);
}
}
}
// CLI entry point - detect if running as main module (ESM)
// In ESM, we use import.meta.url to check if this is the main module
const isMain = process.argv[1] && (
process.argv[1].endsWith('/un_async.js') ||
process.argv[1].endsWith('\\un_async.js') ||
import.meta.url === `file://${process.argv[1]}`
);
if (isMain) {
cliMain().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -53,6 +53,8 @@ Library Usage:
clone_snapshot,
# Key validation
validate_keys,
# Image generation
image,
)
async def main():
@ -1650,3 +1652,682 @@ async def validate_keys(
async with session.post(url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=30)) as resp:
resp.raise_for_status()
return await resp.json()
# =============================================================================
# Image Generation
# =============================================================================
async def image(
prompt: str,
*,
model: str = None,
size: str = "1024x1024",
quality: str = "standard",
n: int = 1,
public_key: str = None,
secret_key: str = None,
) -> Dict[str, Any]:
"""
Generate images from text prompt using AI.
Args:
prompt: Text description of the image to generate
model: Model to use (optional, uses default)
size: Image size (e.g., "1024x1024", "512x512")
quality: "standard" or "hd"
n: Number of images to generate
public_key: API public key (optional)
secret_key: API secret key (optional)
Returns:
dict with keys: images (list of base64 or URLs), created_at
Example:
>>> result = await image("A sunset over mountains")
>>> print(result["images"][0])
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
payload = {
"prompt": prompt,
"size": size,
"quality": quality,
"n": n,
}
if model:
payload["model"] = model
return await _make_request("POST", "/image", public_key, secret_key, payload)
# =============================================================================
# CLI Implementation
# =============================================================================
import sys
import argparse
def _parse_env_file(file_path: str) -> Dict[str, str]:
"""Parse a .env file into a dictionary."""
env_dict = {}
try:
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
# Handle quoted values
value = value.strip()
if (value.startswith('"') and value.endswith('"')) or \
(value.startswith("'") and value.endswith("'")):
value = value[1:-1]
env_dict[key.strip()] = value
except Exception as e:
print(f"Error: Failed to parse env file: {e}", file=sys.stderr)
sys.exit(1)
return env_dict
def _format_list_output(items: List[Dict[str, Any]], resource_type: str) -> str:
"""Format list output in table format."""
if not items:
return f"No {resource_type}s found."
# Determine columns based on resource type
if resource_type == "session":
headers = ["ID", "STATUS", "SHELL", "CREATED"]
rows = []
for item in items:
rows.append([
item.get("id", item.get("session_id", ""))[:36],
item.get("status", "unknown"),
item.get("shell", "bash"),
item.get("created_at", "")[:19] if item.get("created_at") else "",
])
elif resource_type == "service":
headers = ["ID", "NAME", "STATUS", "PORTS", "CREATED"]
rows = []
for item in items:
ports = item.get("ports", [])
ports_str = ",".join(str(p) for p in ports) if ports else ""
rows.append([
item.get("id", item.get("service_id", ""))[:36],
item.get("name", "")[:20],
item.get("status", "unknown"),
ports_str[:15],
item.get("created_at", "")[:19] if item.get("created_at") else "",
])
elif resource_type == "snapshot":
headers = ["ID", "NAME", "TYPE", "SIZE", "CREATED"]
rows = []
for item in items:
rows.append([
item.get("id", item.get("snapshot_id", ""))[:36],
item.get("name", "")[:20],
item.get("source_type", "unknown"),
item.get("size", ""),
item.get("created_at", "")[:19] if item.get("created_at") else "",
])
else:
headers = ["ID", "STATUS"]
rows = [[str(item.get("id", "")), str(item.get("status", ""))] for item in items]
# Calculate column widths
widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(str(cell)))
# Build output
lines = []
header_line = " ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
lines.append(header_line)
for row in rows:
line = " ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(row))
lines.append(line)
return "\n".join(lines)
def _build_parser() -> argparse.ArgumentParser:
"""Build the argument parser for the CLI."""
parser = argparse.ArgumentParser(
prog="un_async.py",
description="Unsandbox CLI (Async) - Execute code in secure containers",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python un_async.py script.py Execute Python script
python un_async.py -s bash 'echo hello' Inline bash command
python un_async.py session --list List active sessions
python un_async.py service --list List all services
python un_async.py snapshot --list List all snapshots
python un_async.py key Check API key
""",
)
# Global options
parser.add_argument("-s", "--shell", metavar="LANG",
help="Language for inline code execution")
parser.add_argument("-e", "--env", action="append", metavar="KEY=VAL",
help="Set environment variable (can be used multiple times)")
parser.add_argument("-f", "--file", action="append", metavar="FILE",
help="Add input file to /tmp/ (can be used multiple times)")
parser.add_argument("-F", "--file-path", action="append", metavar="FILE",
help="Add input file with path preserved")
parser.add_argument("-a", "--artifacts", action="store_true",
help="Return compiled artifacts")
parser.add_argument("-o", "--output", metavar="DIR",
help="Output directory for artifacts")
parser.add_argument("-p", "--public-key", metavar="KEY",
help="API public key")
parser.add_argument("-k", "--secret-key", metavar="KEY",
help="API secret key")
parser.add_argument("-n", "--network", choices=["zerotrust", "semitrusted"],
default="zerotrust", help="Network mode (default: zerotrust)")
parser.add_argument("-v", "--vcpu", type=int, default=1, choices=range(1, 9),
metavar="N", help="vCPU count (1-8, default: 1)")
parser.add_argument("-y", "--yes", action="store_true",
help="Skip confirmation prompts")
# Subcommands
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Session subcommand
session_parser = subparsers.add_parser("session", help="Manage interactive sessions")
session_group = session_parser.add_mutually_exclusive_group()
session_group.add_argument("-l", "--list", action="store_true",
help="List active sessions")
session_group.add_argument("--attach", metavar="ID",
help="Reconnect to existing session")
session_group.add_argument("--kill", metavar="ID",
help="Terminate a session")
session_group.add_argument("--freeze", metavar="ID",
help="Pause session")
session_group.add_argument("--unfreeze", metavar="ID",
help="Resume session")
session_group.add_argument("--boost", metavar="ID",
help="Add resources to session")
session_group.add_argument("--unboost", metavar="ID",
help="Remove boost from session")
session_group.add_argument("--snapshot", metavar="ID",
help="Create snapshot of session")
session_parser.add_argument("--shell", metavar="SHELL",
help="Shell/REPL to use (default: bash)")
session_parser.add_argument("--tmux", action="store_true",
help="Enable persistence with tmux")
session_parser.add_argument("--screen", action="store_true",
help="Enable persistence with screen")
session_parser.add_argument("--snapshot-name", metavar="NAME",
help="Name for snapshot")
session_parser.add_argument("--hot", action="store_true",
help="Live snapshot (no freeze)")
session_parser.add_argument("--audit", action="store_true",
help="Record session")
# Service subcommand
service_parser = subparsers.add_parser("service", help="Manage persistent services")
service_group = service_parser.add_mutually_exclusive_group()
service_group.add_argument("-l", "--list", action="store_true",
help="List all services")
service_group.add_argument("--info", metavar="ID",
help="Get service details")
service_group.add_argument("--logs", metavar="ID",
help="Get all logs")
service_group.add_argument("--tail", metavar="ID",
help="Get last 9000 lines of logs")
service_group.add_argument("--freeze", metavar="ID",
help="Pause service")
service_group.add_argument("--unfreeze", metavar="ID",
help="Resume service")
service_group.add_argument("--destroy", metavar="ID",
help="Delete service")
service_group.add_argument("--lock", metavar="ID",
help="Prevent deletion")
service_group.add_argument("--unlock", metavar="ID",
help="Allow deletion")
service_group.add_argument("--resize", metavar="ID",
help="Resize service (with --vcpu)")
service_group.add_argument("--redeploy", metavar="ID",
help="Re-run bootstrap")
service_group.add_argument("--execute", nargs=2, metavar=("ID", "CMD"),
help="Run command in service")
service_group.add_argument("--snapshot", metavar="ID",
help="Create snapshot of service")
service_parser.add_argument("--name", metavar="NAME",
help="Service name (creates new)")
service_parser.add_argument("--ports", metavar="PORTS",
help="Comma-separated ports")
service_parser.add_argument("--domains", metavar="DOMAINS",
help="Custom domains")
service_parser.add_argument("--type", metavar="TYPE", dest="service_type",
help="Service type (minecraft, tcp, udp)")
service_parser.add_argument("--bootstrap", metavar="CMD",
help="Bootstrap command")
service_parser.add_argument("--bootstrap-file", metavar="FILE",
help="Bootstrap from file")
service_parser.add_argument("--env-file", metavar="FILE",
help="Load env from .env file")
service_parser.add_argument("--snapshot-name", metavar="NAME",
help="Name for snapshot")
service_parser.add_argument("--hot", action="store_true",
help="Live snapshot (no freeze)")
# Service env subcommand
service_env_parser = subparsers.add_parser("service-env",
help="Manage service environment vault")
service_env_parser.add_argument("action", choices=["status", "set", "export", "delete"],
help="Environment action")
service_env_parser.add_argument("service_id", metavar="ID",
help="Service ID")
service_env_parser.add_argument("--env-file", metavar="FILE",
help="Load env from .env file (for set)")
# Snapshot subcommand
snapshot_parser = subparsers.add_parser("snapshot", help="Manage snapshots")
snapshot_group = snapshot_parser.add_mutually_exclusive_group()
snapshot_group.add_argument("-l", "--list", action="store_true",
help="List all snapshots")
snapshot_group.add_argument("--info", metavar="ID",
help="Get snapshot details")
snapshot_group.add_argument("--delete", metavar="ID",
help="Delete snapshot")
snapshot_group.add_argument("--lock", metavar="ID",
help="Prevent deletion")
snapshot_group.add_argument("--unlock", metavar="ID",
help="Allow deletion")
snapshot_group.add_argument("--clone", metavar="ID",
help="Clone snapshot")
snapshot_parser.add_argument("--type", choices=["session", "service"],
dest="clone_type", help="Clone type")
snapshot_parser.add_argument("--name", metavar="NAME",
help="Name for cloned resource")
snapshot_parser.add_argument("--shell", metavar="SHELL",
help="Shell for cloned session")
snapshot_parser.add_argument("--ports", metavar="PORTS",
help="Ports for cloned service")
# Key subcommand
subparsers.add_parser("key", help="Check API key validity")
# Positional argument for source file or inline code
parser.add_argument("source", nargs="?",
help="Source file or inline code (with -s)")
return parser
async def _async_main():
"""Async main entry point for CLI."""
parser = _build_parser()
args = parser.parse_args()
# Resolve credentials
try:
public_key, secret_key = _resolve_credentials(
args.public_key, args.secret_key
)
except CredentialsError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(3)
try:
# Handle subcommands
if args.command == "session":
await _handle_session_command(args, public_key, secret_key)
elif args.command == "service":
await _handle_service_command(args, public_key, secret_key)
elif args.command == "service-env":
await _handle_service_env_command(args, public_key, secret_key)
elif args.command == "snapshot":
await _handle_snapshot_command(args, public_key, secret_key)
elif args.command == "key":
await _handle_key_command(public_key, secret_key)
elif args.source or args.shell:
await _handle_execute_command(args, public_key, secret_key)
else:
parser.print_help()
sys.exit(2)
except CredentialsError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(3)
except aiohttp.ClientResponseError as e:
if e.status == 401:
print("Error: Authentication failed", file=sys.stderr)
sys.exit(3)
print(f"Error: API error - {e}", file=sys.stderr)
sys.exit(4)
except aiohttp.ClientError as e:
print(f"Error: Network error - {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
async def _handle_execute_command(args, public_key: str, secret_key: str):
"""Handle code execution command."""
# Determine language and code
if args.shell:
# Inline code mode
if not args.source:
print("Error: Code required with -s/--shell", file=sys.stderr)
sys.exit(2)
language = args.shell
code = args.source
else:
# File mode
if not args.source:
print("Error: Source file required", file=sys.stderr)
sys.exit(2)
# Detect language from filename
language = detect_language(args.source)
if not language:
print(f"Error: Cannot detect language from '{args.source}'", file=sys.stderr)
sys.exit(2)
# Read source file
try:
with open(args.source, "r") as f:
code = f.read()
except FileNotFoundError:
print(f"Error: File not found: {args.source}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: Failed to read file: {e}", file=sys.stderr)
sys.exit(1)
# Execute code
result = await execute_code(language, code, public_key, secret_key)
# Output result
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
exit_code = result.get("exit_code", 0)
execution_time = result.get("execution_time_ms", 0)
if stdout:
print(stdout, end="")
if not stdout.endswith("\n"):
print()
if stderr:
print(stderr, end="", file=sys.stderr)
if not stderr.endswith("\n"):
print(file=sys.stderr)
print("---")
print(f"Exit code: {exit_code}")
print(f"Execution time: {execution_time}ms")
sys.exit(exit_code if exit_code else 0)
async def _handle_session_command(args, public_key: str, secret_key: str):
"""Handle session subcommand."""
if args.list:
sessions = await list_sessions(public_key, secret_key)
print(_format_list_output(sessions, "session"))
elif args.attach:
# Get session info for attach
session = await get_session(args.attach, public_key, secret_key)
print(f"Session ID: {session.get('id', session.get('session_id', ''))}")
print(f"Status: {session.get('status', 'unknown')}")
print(f"WebSocket URL: wss://api.unsandbox.com/sessions/{args.attach}/shell")
print("\nUse a WebSocket client to connect interactively.")
elif args.kill:
result = await delete_session(args.kill, public_key, secret_key)
print(f"Session {args.kill} terminated")
elif args.freeze:
result = await freeze_session(args.freeze, public_key, secret_key)
print(f"Session {args.freeze} frozen")
elif args.unfreeze:
result = await unfreeze_session(args.unfreeze, public_key, secret_key)
print(f"Session {args.unfreeze} unfrozen")
elif args.boost:
result = await boost_session(args.boost, public_key, secret_key)
print(f"Session {args.boost} boosted")
elif args.unboost:
result = await unboost_session(args.unboost, public_key, secret_key)
print(f"Session {args.unboost} unboosted")
elif args.snapshot:
snapshot_id = await session_snapshot(
args.snapshot, public_key, secret_key,
name=args.snapshot_name,
hot=args.hot
)
print(f"Snapshot created: {snapshot_id}")
else:
# Create new session
multiplexer = None
if args.tmux:
multiplexer = "tmux"
elif args.screen:
multiplexer = "screen"
result = await create_session(
shell=args.shell,
network_mode="semitrusted" if hasattr(args, 'network') and args.network == "semitrusted" else "zerotrust",
public_key=public_key,
secret_key=secret_key,
multiplexer=multiplexer,
)
session_id = result.get("session_id", result.get("id", ""))
print(f"Session created: {session_id}")
print(f"WebSocket URL: wss://api.unsandbox.com/sessions/{session_id}/shell")
async def _handle_service_command(args, public_key: str, secret_key: str):
"""Handle service subcommand."""
if args.list:
services = await list_services(public_key, secret_key)
print(_format_list_output(services, "service"))
elif args.info:
service = await get_service(args.info, public_key, secret_key)
print(json.dumps(service, indent=2))
elif args.logs:
result = await get_service_logs(args.logs, all_logs=True, public_key=public_key, secret_key=secret_key)
print(result.get("log", ""))
elif args.tail:
result = await get_service_logs(args.tail, all_logs=False, public_key=public_key, secret_key=secret_key)
print(result.get("log", ""))
elif args.freeze:
result = await freeze_service(args.freeze, public_key, secret_key)
print(f"Service {args.freeze} frozen")
elif args.unfreeze:
result = await unfreeze_service(args.unfreeze, public_key, secret_key)
print(f"Service {args.unfreeze} unfrozen")
elif args.destroy:
result = await delete_service(args.destroy, public_key, secret_key)
print(f"Service {args.destroy} destroyed")
elif args.lock:
result = await lock_service(args.lock, public_key, secret_key)
print(f"Service {args.lock} locked")
elif args.unlock:
result = await unlock_service(args.unlock, public_key, secret_key)
print(f"Service {args.unlock} unlocked")
elif args.resize:
vcpu = getattr(args, 'vcpu', 1) or 1
result = await update_service(args.resize, public_key, secret_key, vcpu=vcpu)
print(f"Service {args.resize} resized to {vcpu} vCPU(s)")
elif args.redeploy:
bootstrap = None
if args.bootstrap_file:
with open(args.bootstrap_file, "r") as f:
bootstrap = f.read()
elif args.bootstrap:
bootstrap = args.bootstrap
result = await redeploy_service(args.redeploy, bootstrap=bootstrap, public_key=public_key, secret_key=secret_key)
print(f"Service {args.redeploy} redeployed")
elif args.execute:
service_id, command = args.execute
result = await execute_in_service(service_id, command, public_key=public_key, secret_key=secret_key)
# Handle async result
if result.get("job_id"):
job_result = await wait_for_job(result["job_id"], public_key, secret_key)
stdout = job_result.get("stdout", "")
stderr = job_result.get("stderr", "")
if stdout:
print(stdout, end="")
if stderr:
print(stderr, end="", file=sys.stderr)
else:
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
if stdout:
print(stdout, end="")
if stderr:
print(stderr, end="", file=sys.stderr)
elif args.snapshot:
snapshot_id = await service_snapshot(
args.snapshot, public_key, secret_key,
name=getattr(args, 'snapshot_name', None),
hot=getattr(args, 'hot', False)
)
print(f"Snapshot created: {snapshot_id}")
elif args.name:
# Create new service
if not args.ports:
print("Error: --ports required when creating service", file=sys.stderr)
sys.exit(2)
ports = [int(p.strip()) for p in args.ports.split(",")]
bootstrap = None
if args.bootstrap_file:
with open(args.bootstrap_file, "r") as f:
bootstrap = f.read()
elif args.bootstrap:
bootstrap = args.bootstrap
custom_domains = None
if args.domains:
custom_domains = [d.strip() for d in args.domains.split(",")]
result = await create_service(
name=args.name,
ports=ports,
bootstrap=bootstrap,
public_key=public_key,
secret_key=secret_key,
custom_domains=custom_domains,
vcpu=getattr(args, 'vcpu', 1) or 1,
service_type=args.service_type,
)
service_id = result.get("service_id", result.get("id", ""))
print(f"Service created: {service_id}")
print(f"URL: https://{args.name}.on.unsandbox.com")
else:
print("Error: No action specified for service command", file=sys.stderr)
sys.exit(2)
async def _handle_service_env_command(args, public_key: str, secret_key: str):
"""Handle service env subcommand."""
if args.action == "status":
result = await get_service_env(args.service_id, public_key, secret_key)
print(f"Has vault: {result.get('has_vault', False)}")
print(f"Variable count: {result.get('count', 0)}")
if result.get('updated_at'):
print(f"Updated at: {result.get('updated_at')}")
elif args.action == "set":
# Read env from file or stdin
if args.env_file:
env_dict = _parse_env_file(args.env_file)
else:
# Read from stdin
print("Enter environment variables (KEY=VALUE), one per line. Ctrl+D to finish:", file=sys.stderr)
env_dict = {}
for line in sys.stdin:
line = line.strip()
if line and "=" in line:
key, _, value = line.partition("=")
env_dict[key.strip()] = value.strip()
result = await set_service_env(args.service_id, env_dict, public_key, secret_key)
print(f"Environment set: {result.get('count', len(env_dict))} variables")
elif args.action == "export":
result = await export_service_env(args.service_id, public_key, secret_key)
env_content = result.get("env", "")
print(env_content)
elif args.action == "delete":
result = await delete_service_env(args.service_id, public_key=public_key, secret_key=secret_key)
print(f"Environment vault deleted for service {args.service_id}")
async def _handle_snapshot_command(args, public_key: str, secret_key: str):
"""Handle snapshot subcommand."""
if args.list:
snapshots = await list_snapshots(public_key, secret_key)
print(_format_list_output(snapshots, "snapshot"))
elif args.info:
# Get snapshot info via listing and filtering
snapshots = await list_snapshots(public_key, secret_key)
snapshot = next((s for s in snapshots if s.get("id") == args.info or s.get("snapshot_id") == args.info), None)
if snapshot:
print(json.dumps(snapshot, indent=2))
else:
print(f"Error: Snapshot {args.info} not found", file=sys.stderr)
sys.exit(1)
elif args.delete:
result = await delete_snapshot(args.delete, public_key, secret_key)
print(f"Snapshot {args.delete} deleted")
elif args.lock:
result = await lock_snapshot(args.lock, public_key, secret_key)
print(f"Snapshot {args.lock} locked")
elif args.unlock:
result = await unlock_snapshot(args.unlock, public_key, secret_key)
print(f"Snapshot {args.unlock} unlocked")
elif args.clone:
clone_type = args.clone_type or "session"
ports = None
if args.ports:
ports = [int(p.strip()) for p in args.ports.split(",")]
result = await clone_snapshot(
args.clone,
clone_type=clone_type,
name=args.name,
public_key=public_key,
secret_key=secret_key,
shell=args.shell,
ports=ports,
)
if clone_type == "session":
print(f"Session created: {result.get('session_id', result.get('id', ''))}")
else:
print(f"Service created: {result.get('service_id', result.get('id', ''))}")
else:
print("Error: No action specified for snapshot command", file=sys.stderr)
sys.exit(2)
async def _handle_key_command(public_key: str, secret_key: str):
"""Handle key validation command."""
result = await validate_keys(public_key, secret_key)
print(f"Public key: {public_key}")
print(f"Valid: {result.get('valid', False)}")
if result.get('tier'):
print(f"Tier: {result.get('tier')}")
if result.get('expires_at'):
print(f"Expires: {result.get('expires_at')}")
if result.get('reason'):
print(f"Reason: {result.get('reason')}")
def cli_main():
"""Main entry point for CLI - wraps async main."""
asyncio.run(_async_main())
if __name__ == "__main__":
cli_main()

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,14 @@
# snapshot_future = UnAsync.session_snapshot(session_id)
# snapshot_id = snapshot_future.value
#
# CLI Usage:
# ruby un_async.rb script.py # Execute Python script
# ruby un_async.rb -s bash 'echo hello' # Inline bash command
# ruby un_async.rb session --list # List sessions
# ruby un_async.rb service --list # List services
# ruby un_async.rb snapshot --list # List snapshots
# ruby un_async.rb key # Check API key
#
# Authentication Priority (4-tier):
# 1. Method arguments (public_key:, secret_key:)
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
@ -54,6 +62,7 @@ require 'json'
require 'openssl'
require 'fileutils'
require 'thread'
require 'optparse'
# Unsandbox Ruby SDK module (asynchronous)
# Returns Future objects that can be awaited with .value
@ -1090,6 +1099,38 @@ module UnAsync
end
end
# Generate images from text prompt using AI.
#
# @param prompt [String] Text description of the image to generate
# @param model [String, nil] Model to use (optional)
# @param size [String] Image size (default: "1024x1024")
# @param quality [String] "standard" or "hd" (default: "standard")
# @param n [Integer] Number of images to generate (default: 1)
# @param public_key [String, nil] API public key
# @param secret_key [String, nil] API secret key
# @return [Future<Hash>] Future resolving to result with :images array and :created_at
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# result = UnAsync.image("A sunset over mountains").value
# puts result["images"] # Array of image data/URLs
def image(prompt, model: nil, size: '1024x1024', quality: 'standard', n: 1,
public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
payload = {
prompt: prompt,
size: size,
quality: quality,
n: n
}
payload[:model] = model if model
make_request_sync('POST', '/image', pk, sk, payload)
end
end
# Execute multiple futures concurrently and wait for all to complete
#
# @param futures [Array<Future>] Array of futures to wait for

View file

@ -25,6 +25,14 @@
# # Snapshot operations
# snapshot_id = Un.session_snapshot(session_id)
#
# CLI Usage:
# ruby un.rb script.py # Execute Python script
# ruby un.rb -s bash 'echo hello' # Inline bash command
# ruby un.rb session --list # List sessions
# ruby un.rb service --list # List services
# ruby un.rb snapshot --list # List snapshots
# ruby un.rb key # Check API key
#
# Authentication Priority (4-tier):
# 1. Method arguments (public_key:, secret_key:)
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
@ -46,6 +54,7 @@ require 'uri'
require 'json'
require 'openssl'
require 'fileutils'
require 'optparse'
# Unsandbox Ruby SDK module (synchronous)
module Un
@ -929,6 +938,36 @@ module Un
make_request('POST', '/keys/validate', pk, sk, {})
end
# Generate images from text prompt using AI.
#
# @param prompt [String] Text description of the image to generate
# @param model [String, nil] Model to use (optional)
# @param size [String] Image size (default: "1024x1024")
# @param quality [String] "standard" or "hd" (default: "standard")
# @param n [Integer] Number of images to generate (default: 1)
# @param public_key [String, nil] API public key
# @param secret_key [String, nil] API secret key
# @return [Hash] Result with :images array and :created_at
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.image("A sunset over mountains")
# puts result["images"] # Array of image data/URLs
def image(prompt, model: nil, size: '1024x1024', quality: 'standard', n: 1,
public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
payload = {
prompt: prompt,
size: size,
quality: quality,
n: n
}
payload[:model] = model if model
make_request('POST', '/image', pk, sk, payload)
end
private
# Language detection mapping (file extension -> language)
@ -1231,4 +1270,893 @@ module Un
nil
end
end
# ============================================================================
# CLI Implementation
# ============================================================================
# Exit codes
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_INVALID_ARGS = 2
EXIT_AUTH_ERROR = 3
EXIT_API_ERROR = 4
EXIT_TIMEOUT = 5
class << self
# Main CLI entry point
def cli_main
# Global options
options = {
shell: nil,
env: [],
files: [],
file_paths: [],
public_key: nil,
secret_key: nil,
network: 'zerotrust',
vcpu: 1,
yes: false,
artifacts: false,
output: nil
}
# Check for subcommands first
if ARGV.empty?
cli_show_help
exit(EXIT_INVALID_ARGS)
end
case ARGV[0]
when 'session'
ARGV.shift
cli_session(options)
when 'service'
ARGV.shift
cli_service(options)
when 'snapshot'
ARGV.shift
cli_snapshot(options)
when 'key'
ARGV.shift
cli_key(options)
when '-h', '--help', 'help'
cli_show_help
exit(EXIT_SUCCESS)
else
cli_execute(options)
end
rescue CredentialsError => e
$stderr.puts "Error: #{e.message}"
exit(EXIT_AUTH_ERROR)
rescue APIError => e
$stderr.puts "Error: #{e.message}"
exit(EXIT_API_ERROR)
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
$stderr.puts "Error: #{e.message}"
exit(EXIT_INVALID_ARGS)
rescue Interrupt
$stderr.puts "\nInterrupted"
exit(EXIT_ERROR)
end
private
# Show main help
def cli_show_help
puts <<~HELP
unsandbox.com Ruby SDK - Secure Code Execution
Usage:
ruby un.rb [options] <source_file> Execute code file
ruby un.rb [options] -s LANG 'code' Execute inline code
ruby un.rb session [options] Manage sessions
ruby un.rb service [options] Manage services
ruby un.rb snapshot [options] Manage snapshots
ruby un.rb key Check API key
Global Options:
-s, --shell LANG Language for inline code
-e, --env KEY=VAL Set environment variable (can repeat)
-f, --file FILE Add input file to /tmp/
-F, --file-path FILE Add input file with path preserved
-a, --artifacts Return compiled artifacts
-o, --output DIR Output directory for artifacts
-p, --public-key KEY API public key
-k, --secret-key KEY API secret key
-n, --network MODE Network mode: zerotrust or semitrusted
-v, --vcpu N vCPU count (1-8)
-y, --yes Skip confirmation prompts
-h, --help Show this help
Examples:
ruby un.rb script.py
ruby un.rb -s python 'print("hello")'
ruby un.rb -n semitrusted crawler.py
ruby un.rb session --list
ruby un.rb service --name web --ports 80 --bootstrap "python -m http.server 80"
HELP
end
# Parse global options from ARGV
def parse_global_options(options)
OptionParser.new do |opts|
opts.on('-s', '--shell LANG', 'Language for inline code') do |v|
options[:shell] = v
end
opts.on('-e', '--env KEY=VAL', 'Set environment variable') do |v|
options[:env] << v
end
opts.on('-f', '--file FILE', 'Add input file to /tmp/') do |v|
options[:files] << v
end
opts.on('-F', '--file-path FILE', 'Add input file with path preserved') do |v|
options[:file_paths] << v
end
opts.on('-a', '--artifacts', 'Return compiled artifacts') do
options[:artifacts] = true
end
opts.on('-o', '--output DIR', 'Output directory for artifacts') do |v|
options[:output] = v
end
opts.on('-p', '--public-key KEY', 'API public key') do |v|
options[:public_key] = v
end
opts.on('-k', '--secret-key KEY', 'API secret key') do |v|
options[:secret_key] = v
end
opts.on('-n', '--network MODE', 'Network mode') do |v|
options[:network] = v
end
opts.on('-v', '--vcpu N', Integer, 'vCPU count (1-8)') do |v|
options[:vcpu] = v
end
opts.on('-y', '--yes', 'Skip confirmation prompts') do
options[:yes] = true
end
opts.on('-h', '--help', 'Show help') do
yield if block_given?
exit(EXIT_SUCCESS)
end
end
end
# Execute code command
def cli_execute(options)
parser = parse_global_options(options) do
cli_show_help
end
parser.parse!(ARGV)
if ARGV.empty?
$stderr.puts 'Error: No source file or code provided'
exit(EXIT_INVALID_ARGS)
end
code = nil
language = nil
if options[:shell]
# Inline code mode: -s LANG 'code'
language = options[:shell]
code = ARGV.join(' ')
else
# File mode: script.py
filename = ARGV[0]
unless File.exist?(filename)
$stderr.puts "Error: File not found: #{filename}"
exit(EXIT_INVALID_ARGS)
end
code = File.read(filename)
language = detect_language(filename)
unless language
$stderr.puts "Error: Cannot detect language for: #{filename}"
$stderr.puts 'Use -s/--shell to specify language'
exit(EXIT_INVALID_ARGS)
end
end
# Execute the code
result = execute_code(
language,
code,
public_key: options[:public_key],
secret_key: options[:secret_key]
)
# Output results
cli_print_execute_result(result)
end
# Print execution result
def cli_print_execute_result(result)
puts result['stdout'] if result['stdout'] && !result['stdout'].empty?
$stderr.puts result['stderr'] if result['stderr'] && !result['stderr'].empty?
puts '---'
puts "Exit code: #{result['exit_code'] || 0}"
if result['execution_time_ms']
puts "Execution time: #{result['execution_time_ms']}ms"
end
end
# Session subcommand
def cli_session(options)
session_opts = {
list: false,
attach: nil,
kill: nil,
freeze: nil,
unfreeze: nil,
boost: nil,
unboost: nil,
snapshot: nil,
snapshot_name: nil,
hot: false,
tmux: false,
screen: false,
shell: 'bash',
audit: false
}
parser = parse_global_options(options) do
cli_session_help
end
parser.on('-l', '--list', 'List active sessions') do
session_opts[:list] = true
end
parser.on('--attach ID', 'Reconnect to existing session') do |v|
session_opts[:attach] = v
end
parser.on('--kill ID', 'Terminate a session') do |v|
session_opts[:kill] = v
end
parser.on('--freeze ID', 'Pause session') do |v|
session_opts[:freeze] = v
end
parser.on('--unfreeze ID', 'Resume session') do |v|
session_opts[:unfreeze] = v
end
parser.on('--boost ID', 'Add vCPUs/RAM') do |v|
session_opts[:boost] = v
end
parser.on('--unboost ID', 'Remove boost') do |v|
session_opts[:unboost] = v
end
parser.on('--snapshot ID', 'Create snapshot') do |v|
session_opts[:snapshot] = v
end
parser.on('--snapshot-name NAME', 'Name for snapshot') do |v|
session_opts[:snapshot_name] = v
end
parser.on('--hot', 'Live snapshot (no freeze)') do
session_opts[:hot] = true
end
parser.on('--tmux', 'Enable persistence with tmux') do
session_opts[:tmux] = true
end
parser.on('--screen', 'Enable persistence with screen') do
session_opts[:screen] = true
end
parser.on('--shell SHELL', 'Shell/REPL to use') do |v|
session_opts[:shell] = v
end
parser.on('--audit', 'Record session') do
session_opts[:audit] = true
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if session_opts[:list]
sessions = list_sessions(**creds)
cli_print_sessions_table(sessions)
elsif session_opts[:kill]
result = delete_session(session_opts[:kill], **creds)
puts "Session #{session_opts[:kill]} terminated"
elsif session_opts[:freeze]
result = freeze_session(session_opts[:freeze], **creds)
puts "Session #{session_opts[:freeze]} frozen"
elsif session_opts[:unfreeze]
result = unfreeze_session(session_opts[:unfreeze], **creds)
puts "Session #{session_opts[:unfreeze]} unfrozen"
elsif session_opts[:boost]
result = boost_session(session_opts[:boost], **creds)
puts "Session #{session_opts[:boost]} boosted"
elsif session_opts[:unboost]
result = unboost_session(session_opts[:unboost], **creds)
puts "Session #{session_opts[:unboost]} unboosted"
elsif session_opts[:snapshot]
snapshot_id = session_snapshot(
session_opts[:snapshot],
name: session_opts[:snapshot_name],
ephemeral: session_opts[:hot],
**creds
)
puts "Snapshot created: #{snapshot_id}"
elsif session_opts[:attach]
# Attach to existing session - show info
session = get_session(session_opts[:attach], **creds)
puts "Session: #{session['id']}"
puts "Status: #{session['status']}"
puts "WebSocket URL: #{session['websocket_url']}" if session['websocket_url']
puts "\nNote: Use a WebSocket client to connect interactively"
else
# Create new session
multiplexer = nil
multiplexer = 'tmux' if session_opts[:tmux]
multiplexer = 'screen' if session_opts[:screen]
result = create_session(
session_opts[:shell],
network_mode: options[:network],
vcpu: options[:vcpu],
multiplexer: multiplexer,
**creds
)
puts "Session created: #{result['session_id']}"
puts "Container: #{result['container_name']}" if result['container_name']
puts "WebSocket URL: #{result['websocket_url']}" if result['websocket_url']
puts "\nNote: Use a WebSocket client to connect interactively"
end
end
# Print sessions table
def cli_print_sessions_table(sessions)
if sessions.empty?
puts 'No active sessions'
return
end
# Header
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'STATUS', 'CREATED')
sessions.each do |s|
puts format('%-38s %-20s %-10s %-20s',
s['id'] || s['session_id'] || '-',
s['name'] || '-',
s['status'] || s['state'] || '-',
s['created_at'] || '-')
end
end
# Session help
def cli_session_help
puts <<~HELP
Session Management
Usage:
ruby un.rb session [options]
Options:
--shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session
--kill ID Terminate a session
--freeze ID Pause session
--unfreeze ID Resume session
--boost ID Add vCPUs/RAM
--unboost ID Remove boost
--tmux Enable persistence with tmux
--screen Enable persistence with screen
--snapshot ID Create snapshot
--snapshot-name NAME Name for snapshot
--hot Live snapshot (no freeze)
--audit Record session
Examples:
ruby un.rb session # New bash session
ruby un.rb session --shell python3 # Python REPL
ruby un.rb session --tmux # Persistent session
ruby un.rb session --list # List sessions
ruby un.rb session --kill abc123 # Kill session
HELP
end
# Service subcommand
def cli_service(options)
service_opts = {
list: false,
name: nil,
ports: nil,
domains: nil,
type: nil,
bootstrap: nil,
bootstrap_file: nil,
env_file: nil,
info: nil,
logs: nil,
tail: nil,
freeze: nil,
unfreeze: nil,
destroy: nil,
lock: nil,
unlock: nil,
resize: nil,
redeploy: nil,
execute: nil,
execute_cmd: nil,
snapshot: nil,
snapshot_name: nil
}
parser = parse_global_options(options) do
cli_service_help
end
parser.on('-l', '--list', 'List all services') do
service_opts[:list] = true
end
parser.on('--name NAME', 'Service name (creates new)') do |v|
service_opts[:name] = v
end
parser.on('--ports PORTS', 'Comma-separated ports') do |v|
service_opts[:ports] = v.split(',').map(&:to_i)
end
parser.on('--domains DOMAINS', 'Custom domains') do |v|
service_opts[:domains] = v.split(',')
end
parser.on('--type TYPE', 'Service type (minecraft, tcp, udp)') do |v|
service_opts[:type] = v
end
parser.on('--bootstrap CMD', 'Bootstrap command') do |v|
service_opts[:bootstrap] = v
end
parser.on('--bootstrap-file FILE', 'Bootstrap from file') do |v|
service_opts[:bootstrap_file] = v
end
parser.on('--env-file FILE', 'Load env from .env file') do |v|
service_opts[:env_file] = v
end
parser.on('--info ID', 'Get service details') do |v|
service_opts[:info] = v
end
parser.on('--logs ID', 'Get all logs') do |v|
service_opts[:logs] = v
end
parser.on('--tail ID', 'Get last 9000 lines') do |v|
service_opts[:tail] = v
end
parser.on('--freeze ID', 'Pause service') do |v|
service_opts[:freeze] = v
end
parser.on('--unfreeze ID', 'Resume service') do |v|
service_opts[:unfreeze] = v
end
parser.on('--destroy ID', 'Delete service') do |v|
service_opts[:destroy] = v
end
parser.on('--lock ID', 'Prevent deletion') do |v|
service_opts[:lock] = v
end
parser.on('--unlock ID', 'Allow deletion') do |v|
service_opts[:unlock] = v
end
parser.on('--resize ID', 'Resize (with --vcpu)') do |v|
service_opts[:resize] = v
end
parser.on('--redeploy ID', 'Re-run bootstrap') do |v|
service_opts[:redeploy] = v
end
parser.on('--execute ID', 'Run command in service') do |v|
service_opts[:execute] = v
end
parser.on('--snapshot ID', 'Create snapshot') do |v|
service_opts[:snapshot] = v
end
parser.on('--snapshot-name NAME', 'Name for snapshot') do |v|
service_opts[:snapshot_name] = v
end
parser.parse!(ARGV)
# Check for env subcommand
if ARGV[0] == 'env'
ARGV.shift
cli_service_env(options, service_opts)
return
end
# Get command argument for execute
service_opts[:execute_cmd] = ARGV.join(' ') if service_opts[:execute] && !ARGV.empty?
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if service_opts[:list]
services = list_services(**creds)
cli_print_services_table(services)
elsif service_opts[:info]
service = get_service(service_opts[:info], **creds)
cli_print_service_info(service)
elsif service_opts[:logs]
result = get_service_logs(service_opts[:logs], all: true, **creds)
puts result['log'] || result['logs'] || ''
elsif service_opts[:tail]
result = get_service_logs(service_opts[:tail], all: false, **creds)
puts result['log'] || result['logs'] || ''
elsif service_opts[:freeze]
freeze_service(service_opts[:freeze], **creds)
puts "Service #{service_opts[:freeze]} frozen"
elsif service_opts[:unfreeze]
unfreeze_service(service_opts[:unfreeze], **creds)
puts "Service #{service_opts[:unfreeze]} unfrozen"
elsif service_opts[:destroy]
delete_service(service_opts[:destroy], **creds)
puts "Service #{service_opts[:destroy]} destroyed"
elsif service_opts[:lock]
lock_service(service_opts[:lock], **creds)
puts "Service #{service_opts[:lock]} locked"
elsif service_opts[:unlock]
unlock_service(service_opts[:unlock], **creds)
puts "Service #{service_opts[:unlock]} unlocked"
elsif service_opts[:resize]
update_service(service_opts[:resize], vcpu: options[:vcpu], **creds)
puts "Service #{service_opts[:resize]} resized to #{options[:vcpu]} vCPUs"
elsif service_opts[:redeploy]
bootstrap = nil
if service_opts[:bootstrap_file]
bootstrap = File.read(service_opts[:bootstrap_file])
elsif service_opts[:bootstrap]
bootstrap = service_opts[:bootstrap]
end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, **creds)
puts "Service #{service_opts[:redeploy]} redeployed"
elsif service_opts[:execute]
cmd = service_opts[:execute_cmd]
if cmd.nil? || cmd.empty?
$stderr.puts 'Error: No command provided for --execute'
exit(EXIT_INVALID_ARGS)
end
result = execute_in_service(service_opts[:execute], cmd, **creds)
cli_print_execute_result(result)
elsif service_opts[:snapshot]
snapshot_id = service_snapshot(
service_opts[:snapshot],
name: service_opts[:snapshot_name],
**creds
)
puts "Snapshot created: #{snapshot_id}"
elsif service_opts[:name]
# Create new service
bootstrap = service_opts[:bootstrap]
if service_opts[:bootstrap_file]
bootstrap = File.read(service_opts[:bootstrap_file])
end
unless bootstrap
$stderr.puts 'Error: --bootstrap or --bootstrap-file required'
exit(EXIT_INVALID_ARGS)
end
unless service_opts[:ports]
$stderr.puts 'Error: --ports required'
exit(EXIT_INVALID_ARGS)
end
result = create_service(
service_opts[:name],
service_opts[:ports],
bootstrap,
network_mode: options[:network],
vcpu: options[:vcpu],
custom_domains: service_opts[:domains],
service_type: service_opts[:type],
**creds
)
puts "Service created: #{result['service_id']}"
puts "URL: #{result['url']}" if result['url']
else
cli_service_help
exit(EXIT_INVALID_ARGS)
end
end
# Print services table
def cli_print_services_table(services)
if services.empty?
puts 'No services'
return
end
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'STATUS', 'CREATED')
services.each do |s|
puts format('%-38s %-20s %-10s %-20s',
s['id'] || s['service_id'] || '-',
s['name'] || '-',
s['status'] || s['state'] || '-',
s['created_at'] || '-')
end
end
# Print service info
def cli_print_service_info(service)
puts "ID: #{service['id'] || service['service_id']}"
puts "Name: #{service['name']}"
puts "Status: #{service['status'] || service['state']}"
puts "URL: #{service['url']}" if service['url']
puts "Ports: #{service['ports']&.join(', ')}" if service['ports']
puts "vCPU: #{service['vcpu']}" if service['vcpu']
puts "Network: #{service['network_mode']}" if service['network_mode']
puts "Created: #{service['created_at']}" if service['created_at']
puts "Locked: #{service['locked']}" if service.key?('locked')
end
# Service help
def cli_service_help
puts <<~HELP
Service Management
Usage:
ruby un.rb service [options]
ruby un.rb service env <command> <id>
Options:
--name NAME Service name (creates new)
--ports PORTS Comma-separated ports
--domains DOMAINS Custom domains
--type TYPE Service type (minecraft, tcp, udp)
--bootstrap CMD Bootstrap command
--bootstrap-file FILE Bootstrap from file
--env-file FILE Load env from .env file
-l, --list List all services
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--freeze ID Pause service
--unfreeze ID Resume service
--destroy ID Delete service
--lock ID Prevent deletion
--unlock ID Allow deletion
--resize ID Resize (with --vcpu)
--redeploy ID Re-run bootstrap
--execute ID 'cmd' Run command in service
--snapshot ID Create snapshot
Env Subcommands:
ruby un.rb service env status ID Show vault status
ruby un.rb service env set ID Set from --env-file or stdin
ruby un.rb service env export ID Export to stdout
ruby un.rb service env delete ID Delete vault
Examples:
ruby un.rb service --name web --ports 80 --bootstrap "python -m http.server 80"
ruby un.rb service --list
ruby un.rb service --logs abc123
ruby un.rb service --execute abc123 'ls -la'
ruby un.rb service env status abc123
HELP
end
# Service env subcommand
def cli_service_env(options, service_opts)
if ARGV.empty?
$stderr.puts 'Error: env subcommand requires: status, set, export, or delete'
exit(EXIT_INVALID_ARGS)
end
cmd = ARGV.shift
service_id = ARGV.shift
unless service_id
$stderr.puts 'Error: Service ID required'
exit(EXIT_INVALID_ARGS)
end
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
case cmd
when 'status'
result = get_service_env(service_id, **creds)
puts "Has vault: #{result['has_vault'] || false}"
puts "Variables: #{result['count'] || 0}"
puts "Updated: #{result['updated_at']}" if result['updated_at']
when 'set'
env_content = nil
if service_opts[:env_file]
env_content = File.read(service_opts[:env_file])
elsif !$stdin.tty?
env_content = $stdin.read
else
$stderr.puts 'Error: Provide --env-file or pipe content to stdin'
exit(EXIT_INVALID_ARGS)
end
set_service_env(service_id, env_content, **creds)
puts "Environment vault updated for #{service_id}"
when 'export'
result = export_service_env(service_id, **creds)
puts result['env'] || ''
when 'delete'
delete_service_env(service_id, **creds)
puts "Environment vault deleted for #{service_id}"
else
$stderr.puts "Error: Unknown env command: #{cmd}"
exit(EXIT_INVALID_ARGS)
end
end
# Snapshot subcommand
def cli_snapshot(options)
snapshot_opts = {
list: false,
info: nil,
delete: nil,
lock: nil,
unlock: nil,
clone: nil,
clone_type: 'session',
clone_name: nil,
clone_shell: nil,
clone_ports: nil
}
parser = parse_global_options(options) do
cli_snapshot_help
end
parser.on('-l', '--list', 'List all snapshots') do
snapshot_opts[:list] = true
end
parser.on('--info ID', 'Get snapshot details') do |v|
snapshot_opts[:info] = v
end
parser.on('--delete ID', 'Delete snapshot') do |v|
snapshot_opts[:delete] = v
end
parser.on('--lock ID', 'Prevent deletion') do |v|
snapshot_opts[:lock] = v
end
parser.on('--unlock ID', 'Allow deletion') do |v|
snapshot_opts[:unlock] = v
end
parser.on('--clone ID', 'Clone snapshot') do |v|
snapshot_opts[:clone] = v
end
parser.on('--type TYPE', 'Clone type: session or service') do |v|
snapshot_opts[:clone_type] = v
end
parser.on('--name NAME', 'Name for cloned resource') do |v|
snapshot_opts[:clone_name] = v
end
parser.on('--shell SHELL', 'Shell for cloned session') do |v|
snapshot_opts[:clone_shell] = v
end
parser.on('--ports PORTS', 'Ports for cloned service') do |v|
snapshot_opts[:clone_ports] = v.split(',').map(&:to_i)
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if snapshot_opts[:list]
snapshots = list_snapshots(**creds)
cli_print_snapshots_table(snapshots)
elsif snapshot_opts[:info]
# Get snapshot details via restore endpoint or list
snapshots = list_snapshots(**creds)
snapshot = snapshots.find { |s| s['snapshot_id'] == snapshot_opts[:info] || s['id'] == snapshot_opts[:info] }
if snapshot
cli_print_snapshot_info(snapshot)
else
$stderr.puts "Error: Snapshot not found: #{snapshot_opts[:info]}"
exit(EXIT_ERROR)
end
elsif snapshot_opts[:delete]
delete_snapshot(snapshot_opts[:delete], **creds)
puts "Snapshot #{snapshot_opts[:delete]} deleted"
elsif snapshot_opts[:lock]
lock_snapshot(snapshot_opts[:lock], **creds)
puts "Snapshot #{snapshot_opts[:lock]} locked"
elsif snapshot_opts[:unlock]
unlock_snapshot(snapshot_opts[:unlock], **creds)
puts "Snapshot #{snapshot_opts[:unlock]} unlocked"
elsif snapshot_opts[:clone]
result = clone_snapshot(
snapshot_opts[:clone],
type: snapshot_opts[:clone_type],
name: snapshot_opts[:clone_name],
shell: snapshot_opts[:clone_shell],
ports: snapshot_opts[:clone_ports],
**creds
)
if result['session_id']
puts "Session created: #{result['session_id']}"
elsif result['service_id']
puts "Service created: #{result['service_id']}"
else
puts 'Clone completed'
puts JSON.pretty_generate(result)
end
else
cli_snapshot_help
exit(EXIT_INVALID_ARGS)
end
end
# Print snapshots table
def cli_print_snapshots_table(snapshots)
if snapshots.empty?
puts 'No snapshots'
return
end
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'TYPE', 'CREATED')
snapshots.each do |s|
puts format('%-38s %-20s %-10s %-20s',
s['snapshot_id'] || s['id'] || '-',
s['name'] || '-',
s['type'] || s['source_type'] || '-',
s['created_at'] || '-')
end
end
# Print snapshot info
def cli_print_snapshot_info(snapshot)
puts "ID: #{snapshot['snapshot_id'] || snapshot['id']}"
puts "Name: #{snapshot['name']}" if snapshot['name']
puts "Type: #{snapshot['type'] || snapshot['source_type']}"
puts "Source ID: #{snapshot['source_id']}" if snapshot['source_id']
puts "Size: #{snapshot['size']}" if snapshot['size']
puts "Locked: #{snapshot['locked']}" if snapshot.key?('locked')
puts "Created: #{snapshot['created_at']}" if snapshot['created_at']
end
# Snapshot help
def cli_snapshot_help
puts <<~HELP
Snapshot Management
Usage:
ruby un.rb snapshot [options]
Options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete snapshot
--lock ID Prevent deletion
--unlock ID Allow deletion
--clone ID Clone snapshot
--type TYPE Clone type: session or service
--name NAME Name for cloned resource
--shell SHELL Shell for cloned session
--ports PORTS Ports for cloned service
Examples:
ruby un.rb snapshot --list
ruby un.rb snapshot --clone abc123 --type service --name myapp --ports 80
HELP
end
# Key command
def cli_key(options)
parser = parse_global_options(options) do
puts 'Usage: ruby un.rb key [-p PUBLIC_KEY] [-k SECRET_KEY]'
puts
puts 'Check API key validity and show account info'
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
begin
result = validate_keys(**creds)
puts "Valid: #{result['valid']}"
puts "Account: #{result['account'] || result['account_id']}" if result['account'] || result['account_id']
puts "Email: #{result['email']}" if result['email']
puts "Plan: #{result['plan']}" if result['plan']
puts "Credits: #{result['credits']}" if result['credits']
rescue APIError => e
if e.status_code == 401 || e.status_code == 403
puts 'Valid: false'
puts "Error: #{e.message}"
exit(EXIT_AUTH_ERROR)
end
raise
end
end
end
end
# CLI entry point
if __FILE__ == $0
Un.cli_main
end

View file

@ -397,6 +397,28 @@ pub struct ServiceUpdateOptions {
pub vcpu: Option<u32>,
}
/// Options for AI image generation
#[derive(Debug, Clone, Default)]
pub struct ImageOptions {
/// Model to use (optional)
pub model: Option<String>,
/// Image size (default: "1024x1024")
pub size: Option<String>,
/// Quality: "standard" or "hd" (default: "standard")
pub quality: Option<String>,
/// Number of images to generate (default: 1)
pub n: Option<i32>,
}
/// Result of image generation
#[derive(Debug, Clone, Deserialize)]
pub struct ImageResult {
/// Generated images (base64 or URLs)
pub images: Vec<String>,
/// Timestamp when images were created
pub created_at: String,
}
// =============================================================================
// Internal Response Types
// =============================================================================
@ -1824,6 +1846,51 @@ pub async fn validate_keys(creds: &Credentials) -> Result<KeysValid> {
Ok(result)
}
// =============================================================================
// AI Image Generation API Functions
// =============================================================================
/// Generate images from a text prompt using AI.
///
/// # Arguments
/// * `prompt` - Text description of the image to generate
/// * `creds` - API credentials
/// * `opts` - Optional generation parameters
///
/// # Returns
/// * `Result<ImageResult>` - Generated images
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = image("A sunset over mountains", &creds, None).await?;
/// for img in result.images {
/// println!("Image: {}", img);
/// }
///
/// // With options
/// let opts = ImageOptions {
/// size: Some("512x512".to_string()),
/// quality: Some("hd".to_string()),
/// n: Some(2),
/// ..Default::default()
/// };
/// let result = image("A futuristic city", &creds, Some(opts)).await?;
/// ```
pub async fn image(prompt: &str, creds: &Credentials, opts: Option<ImageOptions>) -> Result<ImageResult> {
let opts = opts.unwrap_or_default();
let payload = serde_json::json!({
"prompt": prompt,
"size": opts.size.unwrap_or_else(|| "1024x1024".to_string()),
"quality": opts.quality.unwrap_or_else(|| "standard".to_string()),
"n": opts.n.unwrap_or(1),
"model": opts.model,
});
make_request("POST", "/image", creds, Some(&payload)).await
}
// =============================================================================
// Tests
// =============================================================================

File diff suppressed because it is too large Load diff

View file

@ -1,637 +0,0 @@
#!/usr/bin/env -S deno run --allow-read --allow-env --allow-net
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
//
// This is free public domain software for the public good of a permacomputer hosted
// at permacomputer.com - an always-on computer by the people, for the people. One
// which is durable, easy to repair, and distributed like tap water for machine
// learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around four values:
//
// TRUTH - First principles, math & science, open source code freely distributed
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
// LOVE - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by enabling code execution across 42+
// programming languages through a unified interface, accessible to all. Code is
// seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
// software, either in source code form or as a compiled binary, for any purpose,
// commercial or non-commercial, and by any means.
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// That said, our permacomputer's digital membrane stratum continuously runs unit,
// integration, and functional tests on all of it's own software - with our
// permacomputer monitoring itself, repairing itself, with minimal human in the
// loop guidance. Our agents do their best.
//
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.timehexon.com
// https://www.foxhop.net
// https://www.unturf.com/software
// unsandbox CLI - Deno TypeScript implementation
// Full-featured CLI matching un.c/un.py capabilities
const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com";
const BLUE = "\x1b[34m";
const RED = "\x1b[31m";
const GREEN = "\x1b[32m";
const YELLOW = "\x1b[33m";
const RESET = "\x1b[0m";
const EXT_MAP: Record<string, string> = {
py: "python", js: "javascript", ts: "typescript",
rb: "ruby", php: "php", pl: "perl", lua: "lua",
sh: "bash", go: "go", rs: "rust", c: "c",
cpp: "cpp", cc: "cpp", cxx: "cpp",
java: "java", kt: "kotlin", cs: "csharp", fs: "fsharp",
hs: "haskell", ml: "ocaml", clj: "clojure", scm: "scheme",
lisp: "commonlisp", erl: "erlang", ex: "elixir", exs: "elixir",
jl: "julia", r: "r", R: "r", cr: "crystal",
d: "d", nim: "nim", zig: "zig", v: "vlang",
dart: "dart", groovy: "groovy", scala: "scala",
f90: "fortran", f95: "fortran", cob: "cobol",
pro: "prolog", forth: "forth", "4th": "forth",
tcl: "tcl", raku: "raku", pl6: "raku", p6: "raku",
m: "objc",
};
interface ApiKeys {
publicKey: string;
secretKey: string;
}
function getApiKeys(): ApiKeys {
let publicKey = Deno.env.get("UNSANDBOX_PUBLIC_KEY");
let secretKey = Deno.env.get("UNSANDBOX_SECRET_KEY");
if (!publicKey || !secretKey) {
const oldKey = Deno.env.get("UNSANDBOX_API_KEY");
if (oldKey) {
publicKey = oldKey;
secretKey = oldKey;
} else {
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
Deno.exit(1);
}
}
return { publicKey, secretKey };
}
function detectLanguage(filename: string): string {
const ext = filename.split(".").pop();
if (!ext) {
console.error(`${RED}Error: No file extension found${RESET}`);
Deno.exit(1);
}
const language = EXT_MAP[ext];
if (!language) {
console.error(`${RED}Error: Unknown file extension '${ext}'${RESET}`);
Deno.exit(1);
}
return language;
}
async function apiRequest(
endpoint: string,
method: string,
data?: unknown,
keys?: ApiKeys,
baseUrl?: string,
): Promise<any> {
const base = baseUrl || API_BASE;
const url = `${base}${endpoint}`;
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
// Parse URL to get pathname and search
const urlObj = new URL(url);
const message = `${timestamp}:${method}:${urlObj.pathname}${urlObj.search}:${body}`;
// Create HMAC signature using Web Crypto API
const encoder = new TextEncoder();
const keyData = encoder.encode(keys!.secretKey);
const messageData = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
const signature = Array.from(new Uint8Array(signatureBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
const headers: Record<string, string> = {
Authorization: `Bearer ${keys!.publicKey}`,
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json",
};
const options: RequestInit = {
method,
headers,
};
if (data && method !== "GET") {
options.body = body;
}
const response = await fetch(url, options);
if (!response.ok) {
console.error(`${RED}Error: HTTP ${response.status}${RESET}`);
const errorText = await response.text();
console.error(errorText);
Deno.exit(1);
}
return await response.json();
}
async function cmdExecute(args: string[]) {
const keys = getApiKeys();
let sourceFile = "";
const envVars: Record<string, string> = {};
const inputFiles: string[] = [];
let artifacts = false;
let outputDir = ".";
let network = "";
let vcpu = 0;
// Parse arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "-e":
if (i + 1 < args.length) {
const [key, ...valueParts] = args[++i].split("=");
envVars[key] = valueParts.join("=");
}
break;
case "-f":
if (i + 1 < args.length) {
inputFiles.push(args[++i]);
}
break;
case "-a":
artifacts = true;
break;
case "-o":
if (i + 1 < args.length) {
outputDir = args[++i];
}
break;
case "-n":
if (i + 1 < args.length) {
network = args[++i];
}
break;
case "-v":
if (i + 1 < args.length) {
vcpu = parseInt(args[++i]);
}
break;
default:
sourceFile = arg;
}
}
if (!sourceFile) {
console.error("Usage: un_deno.ts [options] <source_file>");
Deno.exit(1);
}
try {
await Deno.stat(sourceFile);
} catch {
console.error(`${RED}Error: File not found: ${sourceFile}${RESET}`);
Deno.exit(1);
}
// Read source file
const code = await Deno.readTextFile(sourceFile);
const language = detectLanguage(sourceFile);
// Build request payload
const payload: any = {
language,
code,
};
if (Object.keys(envVars).length > 0) {
payload.env = envVars;
}
if (inputFiles.length > 0) {
const files = [];
for (const filepath of inputFiles) {
try {
await Deno.stat(filepath);
} catch {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
Deno.exit(1);
}
const content = await Deno.readFile(filepath);
const encoder = new TextDecoder("latin1");
const b64Content = btoa(encoder.decode(content));
files.push({
filename: filepath.split("/").pop(),
content_base64: b64Content,
});
}
payload.input_files = files;
}
if (artifacts) {
payload.return_artifacts = true;
}
if (network) {
payload.network = network;
}
if (vcpu > 0) {
payload.vcpu = vcpu;
}
// Execute
const result = await apiRequest("/execute", "POST", payload, keys);
// Print output
if (result.stdout) {
Deno.stdout.writeSync(new TextEncoder().encode(`${BLUE}${result.stdout}${RESET}`));
}
if (result.stderr) {
Deno.stderr.writeSync(new TextEncoder().encode(`${RED}${result.stderr}${RESET}`));
}
// Save artifacts
if (artifacts && result.artifacts) {
await Deno.mkdir(outputDir, { recursive: true });
for (const artifact of result.artifacts) {
const filename = artifact.filename;
const decoder = new TextDecoder("latin1");
const content = Uint8Array.from(atob(artifact.content_base64), (c) => c.charCodeAt(0));
const path = `${outputDir}/${filename}`;
await Deno.writeFile(path, content, { mode: 0o755 });
console.error(`${GREEN}Saved: ${path}${RESET}`);
}
}
Deno.exit(result.exit_code || 0);
}
async function cmdSession(args: string[]) {
const keys = getApiKeys();
let listMode = false;
let killId = "";
let shell = "";
let network = "";
let vcpu = 0;
// Parse arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--list":
listMode = true;
break;
case "--kill":
if (i + 1 < args.length) {
killId = args[++i];
}
break;
case "--shell":
if (i + 1 < args.length) {
shell = args[++i];
}
break;
case "-n":
if (i + 1 < args.length) {
network = args[++i];
}
break;
case "-v":
if (i + 1 < args.length) {
vcpu = parseInt(args[++i]);
}
break;
}
}
if (listMode) {
const result = await apiRequest("/sessions", "GET", undefined, keys);
const sessions = result.sessions || [];
if (sessions.length === 0) {
console.log("No active sessions");
} else {
console.log(
`${"ID".padEnd(40)} ${"Shell".padEnd(10)} ${"Status".padEnd(10)} Created`,
);
for (const s of sessions) {
console.log(
`${s.id.padEnd(40)} ${s.shell.padEnd(10)} ${s.status.padEnd(10)} ${s.created_at}`,
);
}
}
return;
}
if (killId) {
await apiRequest(`/sessions/${killId}`, "DELETE", undefined, keys);
console.log(`${GREEN}Session terminated: ${killId}${RESET}`);
return;
}
// Create new session
const payload: any = {
shell: shell || "bash",
};
if (network) payload.network = network;
if (vcpu > 0) payload.vcpu = vcpu;
console.log(`${YELLOW}Creating session...${RESET}`);
const result = await apiRequest("/sessions", "POST", payload, keys);
console.log(`${GREEN}Session created: ${result.id}${RESET}`);
console.log(
`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`,
);
}
async function cmdKey(args: string[]) {
const keys = getApiKeys();
let extend = false;
// Parse arguments
for (let i = 0; i < args.length; i++) {
if (args[i] === "--extend") {
extend = true;
}
}
try {
const result = await apiRequest("/keys/validate", "POST", undefined, keys, PORTAL_BASE);
// Handle --extend flag
if (extend) {
const publicKey = result.public_key;
if (publicKey) {
const url = `${PORTAL_BASE}/keys/extend?pk=${publicKey}`;
console.log(`${BLUE}Opening browser to extend key...${RESET}`);
if (Deno.build.os === "darwin") {
await new Deno.Command("open", { args: [url] }).output();
} else if (Deno.build.os === "linux") {
await new Deno.Command("xdg-open", { args: [url] }).output();
} else if (Deno.build.os === "windows") {
await new Deno.Command("cmd", { args: ["/c", "start", url] }).output();
} else {
console.log(`${YELLOW}Please open manually: ${url}${RESET}`);
}
return;
} else {
console.error(`${RED}Error: Could not retrieve public key${RESET}`);
Deno.exit(1);
}
}
// Check if key is expired
if (result.expired) {
console.log(`${RED}Expired${RESET}`);
console.log(`Public Key: ${result.public_key || "N/A"}`);
console.log(`Tier: ${result.tier || "N/A"}`);
console.log(`Expired: ${result.expires_at || "N/A"}`);
console.log(`${YELLOW}To renew: Visit ${PORTAL_BASE}/keys/extend${RESET}`);
Deno.exit(1);
}
// Valid key
console.log(`${GREEN}Valid${RESET}`);
console.log(`Public Key: ${result.public_key || "N/A"}`);
console.log(`Tier: ${result.tier || "N/A"}`);
console.log(`Status: ${result.status || "N/A"}`);
console.log(`Expires: ${result.expires_at || "N/A"}`);
console.log(`Time Remaining: ${result.time_remaining || "N/A"}`);
console.log(`Rate Limit: ${result.rate_limit || "N/A"}`);
console.log(`Burst: ${result.burst || "N/A"}`);
console.log(`Concurrency: ${result.concurrency || "N/A"}`);
} catch (e) {
console.log(`${RED}Invalid${RESET}`);
console.log(`Reason: ${e}`);
Deno.exit(1);
}
}
async function cmdService(args: string[]) {
const keys = getApiKeys();
let listMode = false;
let infoId = "";
let logsId = "";
let sleepId = "";
let wakeId = "";
let destroyId = "";
let name = "";
let ports = "";
let serviceType = "";
let bootstrap = "";
let network = "";
let vcpu = 0;
// Parse arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--list":
listMode = true;
break;
case "--info":
if (i + 1 < args.length) {
infoId = args[++i];
}
break;
case "--logs":
if (i + 1 < args.length) {
logsId = args[++i];
}
break;
case "--freeze":
if (i + 1 < args.length) {
sleepId = args[++i];
}
break;
case "--unfreeze":
if (i + 1 < args.length) {
wakeId = args[++i];
}
break;
case "--destroy":
if (i + 1 < args.length) {
destroyId = args[++i];
}
break;
case "--name":
if (i + 1 < args.length) {
name = args[++i];
}
break;
case "--ports":
if (i + 1 < args.length) {
ports = args[++i];
}
break;
case "--type":
if (i + 1 < args.length) {
serviceType = args[++i];
}
break;
case "--bootstrap":
if (i + 1 < args.length) {
bootstrap = args[++i];
}
break;
case "-n":
if (i + 1 < args.length) {
network = args[++i];
}
break;
case "-v":
if (i + 1 < args.length) {
vcpu = parseInt(args[++i]);
}
break;
}
}
if (listMode) {
const result = await apiRequest("/services", "GET", undefined, keys);
const services = result.services || [];
if (services.length === 0) {
console.log("No services");
} else {
console.log(
`${"ID".padEnd(20)} ${"Name".padEnd(15)} ${"Status".padEnd(10)} ${"Ports".padEnd(15)} Domains`,
);
for (const s of services) {
const portStr = (s.ports || []).join(",");
const domainStr = (s.domains || []).join(",");
console.log(
`${s.id.padEnd(20)} ${s.name.padEnd(15)} ${s.status.padEnd(10)} ${portStr.padEnd(15)} ${domainStr}`,
);
}
}
return;
}
if (infoId) {
const result = await apiRequest(`/services/${infoId}`, "GET", undefined, keys);
console.log(JSON.stringify(result, null, 2));
return;
}
if (logsId) {
const result = await apiRequest(`/services/${logsId}/logs`, "GET", undefined, keys);
console.log(result.logs || "");
return;
}
if (sleepId) {
await apiRequest(`/services/${sleepId}/freeze`, "POST", undefined, keys);
console.log(`${GREEN}Service sleeping: ${sleepId}${RESET}`);
return;
}
if (wakeId) {
await apiRequest(`/services/${wakeId}/unfreeze`, "POST", undefined, keys);
console.log(`${GREEN}Service waking: ${wakeId}${RESET}`);
return;
}
if (destroyId) {
await apiRequest(`/services/${destroyId}`, "DELETE", undefined, keys);
console.log(`${GREEN}Service destroyed: ${destroyId}${RESET}`);
return;
}
// Create new service
if (name) {
const payload: any = { name };
if (ports) {
payload.ports = ports.split(",").map((p) => parseInt(p));
}
if (serviceType) {
payload.service_type = serviceType;
}
if (bootstrap) {
// Check if bootstrap is a file
try {
const stat = await Deno.stat(bootstrap);
if (stat.isFile) {
payload.bootstrap = await Deno.readTextFile(bootstrap);
} else {
payload.bootstrap = bootstrap;
}
} catch {
payload.bootstrap = bootstrap;
}
}
if (network) payload.network = network;
if (vcpu > 0) payload.vcpu = vcpu;
const result = await apiRequest("/services", "POST", payload, keys);
console.log(`${GREEN}Service created: ${result.id}${RESET}`);
console.log(`Name: ${result.name}`);
if (result.url) {
console.log(`URL: ${result.url}`);
}
return;
}
console.error(
`${RED}Error: Specify --name to create a service, or use --list, --info, etc.${RESET}`,
);
Deno.exit(1);
}
async function main() {
const args = Deno.args;
if (args.length === 0) {
console.error("Usage: un_deno.ts [options] <source_file>");
console.error(" un_deno.ts session [options]");
console.error(" un_deno.ts service [options]");
console.error(" un_deno.ts key [options]");
Deno.exit(1);
}
const firstArg = args[0];
if (firstArg === "session") {
await cmdSession(args.slice(1));
} else if (firstArg === "service") {
await cmdService(args.slice(1));
} else if (firstArg === "key") {
await cmdKey(args.slice(1));
} else {
await cmdExecute(args);
}
}
main();