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:
parent
1fda4110b1
commit
6d91e987ce
17 changed files with 14170 additions and 637 deletions
269
clients/CLI_SPEC.md
Normal file
269
clients/CLI_SPEC.md
Normal 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
318
clients/README.md
Normal 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
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
637
un_deno.ts
637
un_deno.ts
|
|
@ -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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue