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<>());
|
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
|
// HTTP Request Helpers
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -1803,4 +1846,840 @@ public class UnsandboxAsync {
|
||||||
return false;
|
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<>());
|
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
|
// HTTP Request Helpers
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -1807,4 +1853,836 @@ public class Un {
|
||||||
|
|
||||||
return parseJson(responseBody);
|
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, {});
|
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
|
// ES Module exports
|
||||||
export {
|
export {
|
||||||
// Code execution
|
// Code execution
|
||||||
|
|
@ -1116,9 +1145,13 @@ export {
|
||||||
cloneSnapshot,
|
cloneSnapshot,
|
||||||
// Key validation
|
// Key validation
|
||||||
validateKeys,
|
validateKeys,
|
||||||
|
// Image generation
|
||||||
|
image,
|
||||||
// Errors
|
// Errors
|
||||||
CredentialsError,
|
CredentialsError,
|
||||||
TimeoutError,
|
TimeoutError,
|
||||||
|
// CLI
|
||||||
|
cliMain,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default export for convenience
|
// Default export for convenience
|
||||||
|
|
@ -1170,7 +1203,929 @@ export default {
|
||||||
cloneSnapshot,
|
cloneSnapshot,
|
||||||
// Key validation
|
// Key validation
|
||||||
validateKeys,
|
validateKeys,
|
||||||
|
// Image generation
|
||||||
|
image,
|
||||||
// Errors
|
// Errors
|
||||||
CredentialsError,
|
CredentialsError,
|
||||||
TimeoutError,
|
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,
|
clone_snapshot,
|
||||||
# Key validation
|
# Key validation
|
||||||
validate_keys,
|
validate_keys,
|
||||||
|
# Image generation
|
||||||
|
image,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def main():
|
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:
|
async with session.post(url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
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_future = UnAsync.session_snapshot(session_id)
|
||||||
# snapshot_id = snapshot_future.value
|
# 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):
|
# Authentication Priority (4-tier):
|
||||||
# 1. Method arguments (public_key:, secret_key:)
|
# 1. Method arguments (public_key:, secret_key:)
|
||||||
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
|
@ -54,6 +62,7 @@ require 'json'
|
||||||
require 'openssl'
|
require 'openssl'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
require 'thread'
|
require 'thread'
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
# Unsandbox Ruby SDK module (asynchronous)
|
# Unsandbox Ruby SDK module (asynchronous)
|
||||||
# Returns Future objects that can be awaited with .value
|
# Returns Future objects that can be awaited with .value
|
||||||
|
|
@ -1090,6 +1099,38 @@ module UnAsync
|
||||||
end
|
end
|
||||||
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
|
# Execute multiple futures concurrently and wait for all to complete
|
||||||
#
|
#
|
||||||
# @param futures [Array<Future>] Array of futures to wait for
|
# @param futures [Array<Future>] Array of futures to wait for
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@
|
||||||
# # Snapshot operations
|
# # Snapshot operations
|
||||||
# snapshot_id = Un.session_snapshot(session_id)
|
# 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):
|
# Authentication Priority (4-tier):
|
||||||
# 1. Method arguments (public_key:, secret_key:)
|
# 1. Method arguments (public_key:, secret_key:)
|
||||||
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
|
@ -46,6 +54,7 @@ require 'uri'
|
||||||
require 'json'
|
require 'json'
|
||||||
require 'openssl'
|
require 'openssl'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
# Unsandbox Ruby SDK module (synchronous)
|
# Unsandbox Ruby SDK module (synchronous)
|
||||||
module Un
|
module Un
|
||||||
|
|
@ -929,6 +938,36 @@ module Un
|
||||||
make_request('POST', '/keys/validate', pk, sk, {})
|
make_request('POST', '/keys/validate', pk, sk, {})
|
||||||
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 [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
|
private
|
||||||
|
|
||||||
# Language detection mapping (file extension -> language)
|
# Language detection mapping (file extension -> language)
|
||||||
|
|
@ -1231,4 +1270,893 @@ module Un
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,28 @@ pub struct ServiceUpdateOptions {
|
||||||
pub vcpu: Option<u32>,
|
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
|
// Internal Response Types
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -1824,6 +1846,51 @@ pub async fn validate_keys(creds: &Credentials) -> Result<KeysValid> {
|
||||||
Ok(result)
|
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
|
// 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