un-inception/clients/go/async/src/un_async.go

2978 lines
76 KiB
Go

// This is free 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, & 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.
package un_async
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"time"
)
const (
APIBase = "https://api.unsandbox.com"
LanguagesCacheTTL = 3600 // 1 hour in seconds
)
var (
// PollDelaysMs defines exponential backoff delays for job polling
PollDelaysMs = []int{300, 450, 700, 900, 650, 1600, 2000}
// LanguageMap maps file extensions to programming languages
LanguageMap = map[string]string{
"py": "python",
"js": "javascript",
"ts": "typescript",
"rb": "ruby",
"php": "php",
"pl": "perl",
"sh": "bash",
"r": "r",
"R": "r",
"lua": "lua",
"go": "go",
"rs": "rust",
"c": "c",
"cpp": "cpp",
"cc": "cpp",
"cxx": "cpp",
"java": "java",
"kt": "kotlin",
"m": "objc",
"cs": "csharp",
"fs": "fsharp",
"hs": "haskell",
"ml": "ocaml",
"clj": "clojure",
"scm": "scheme",
"ss": "scheme",
"erl": "erlang",
"ex": "elixir",
"exs": "elixir",
"jl": "julia",
"d": "d",
"nim": "nim",
"zig": "zig",
"v": "v",
"cr": "crystal",
"dart": "dart",
"groovy": "groovy",
"f90": "fortran",
"f95": "fortran",
"lisp": "commonlisp",
"lsp": "commonlisp",
"cob": "cobol",
"tcl": "tcl",
"raku": "raku",
"pro": "prolog",
"p": "prolog",
"4th": "forth",
"forth": "forth",
"fth": "forth",
}
)
// CredentialsError is returned when credentials cannot be found or are invalid
type CredentialsError struct {
Message string
}
func (e *CredentialsError) Error() string {
return e.Message
}
// Credentials holds the public and secret API keys
type Credentials struct {
PublicKey string
SecretKey string
}
// Result types for channel-based async responses
// ExecuteResult contains the result of an execution request
type ExecuteResult struct {
Data map[string]interface{}
Err error
}
// JobIDResult contains the result of an async execution request
type JobIDResult struct {
JobID string
Err error
}
// JobResult contains the result of a job query
type JobResult struct {
Data map[string]interface{}
Err error
}
// JobListResult contains the result of listing jobs
type JobListResult struct {
Jobs []map[string]interface{}
Err error
}
// LanguagesResult contains the result of a languages query
type LanguagesResult struct {
Languages []string
Err error
}
// SnapshotResult contains the result of a snapshot operation
type SnapshotResult struct {
SnapshotID string
Err error
}
// SnapshotListResult contains the result of listing snapshots
type SnapshotListResult struct {
Snapshots []map[string]interface{}
Err error
}
// RestoreResult contains the result of restoring a snapshot
type RestoreResult struct {
Data map[string]interface{}
Err error
}
// DeleteResult contains the result of a delete operation
type DeleteResult struct {
Data map[string]interface{}
Err error
}
// CancelResult contains the result of cancelling a job
type CancelResult struct {
Data map[string]interface{}
Err error
}
// getUnsandboxDir returns ~/.unsandbox directory path, creating if necessary
func getUnsandboxDir() (string, error) {
currentUser, err := user.Current()
if err != nil {
return "", err
}
dir := filepath.Join(currentUser.HomeDir, ".unsandbox")
if err := os.MkdirAll(dir, 0700); err != nil {
return "", err
}
return dir, nil
}
// loadCredentialsFromCsv loads credentials from CSV file (public_key,secret_key per line)
func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
data, err := os.ReadFile(csvPath)
if err != nil {
return nil
}
lines := strings.Split(string(data), "\n")
currentIndex := 0
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if currentIndex == accountIndex {
parts := strings.Split(line, ",")
if len(parts) >= 2 {
return &Credentials{
PublicKey: strings.TrimSpace(parts[0]),
SecretKey: strings.TrimSpace(parts[1]),
}
}
}
currentIndex++
}
return nil
}
// ResolveCredentials resolves credentials from 4-tier priority system.
//
// Priority:
// 1. Credentials struct fields (PublicKey, SecretKey non-empty)
// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
// 3. ~/.unsandbox/accounts.csv
// 4. ./accounts.csv
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
// Tier 1: Function arguments
if publicKey != "" && secretKey != "" {
return &Credentials{
PublicKey: publicKey,
SecretKey: secretKey,
}, nil
}
// Tier 2: Environment variables
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
if envPk != "" && envSk != "" {
return &Credentials{
PublicKey: envPk,
SecretKey: envSk,
}, nil
}
// Determine account index
accountIndex := 0
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
var err error
accountIndex, err = strconv.Atoi(envAccount)
if err != nil {
accountIndex = 0
}
}
// Tier 3: ~/.unsandbox/accounts.csv
unsandboxDir, err := getUnsandboxDir()
if err == nil {
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
return creds, nil
}
}
// Tier 4: ./accounts.csv
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
return creds, nil
}
return nil, &CredentialsError{
Message: "No credentials found. Please provide via:\n" +
" 1. Function arguments (publicKey, secretKey)\n" +
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
" 3. ~/.unsandbox/accounts.csv\n" +
" 4. ./accounts.csv",
}
}
// signRequest signs a request using HMAC-SHA256
//
// Message format: "timestamp:METHOD:path:body"
// Returns: 64-character hex string
func signRequest(secretKey string, timestamp int64, method, path string, body []byte) string {
bodyStr := ""
if body != nil {
bodyStr = string(body)
}
message := fmt.Sprintf("%d:%s:%s:%s", timestamp, method, path, bodyStr)
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}
// makeRequest makes an authenticated HTTP request to the API (synchronous, internal use)
func makeRequest(method, path string, creds *Credentials, data interface{}) (map[string]interface{}, error) {
url := APIBase + path
timestamp := time.Now().Unix()
var body []byte
var err error
if data != nil {
body, err = json.Marshal(data)
if err != nil {
return nil, err
}
}
signature := signRequest(creds.SecretKey, timestamp, method, path, body)
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", creds.PublicKey))
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
req.Header.Set("X-Signature", signature)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "un-go-async/2.0")
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return result, nil
}
// getLanguagesCachePath returns path to languages cache file
func getLanguagesCachePath() (string, error) {
unsandboxDir, err := getUnsandboxDir()
if err != nil {
return "", err
}
return filepath.Join(unsandboxDir, "languages.json"), nil
}
// loadLanguagesCache loads languages from cache if valid (< 1 hour old)
func loadLanguagesCache() ([]string, bool) {
cachePath, err := getLanguagesCachePath()
if err != nil {
return nil, false
}
data, err := os.ReadFile(cachePath)
if err != nil {
return nil, false
}
stat, err := os.Stat(cachePath)
if err != nil {
return nil, false
}
ageSeconds := int(time.Since(stat.ModTime()).Seconds())
if ageSeconds >= LanguagesCacheTTL {
return nil, false
}
var cacheData map[string]interface{}
if err := json.Unmarshal(data, &cacheData); err != nil {
return nil, false
}
if langs, ok := cacheData["languages"].([]interface{}); ok {
result := make([]string, len(langs))
for i, lang := range langs {
if s, ok := lang.(string); ok {
result[i] = s
}
}
return result, true
}
return nil, false
}
// saveLanguagesCache saves languages to cache
func saveLanguagesCache(languages []string) {
cachePath, err := getLanguagesCachePath()
if err != nil {
return
}
cacheData := map[string]interface{}{
"languages": languages,
"timestamp": time.Now().Unix(),
}
data, err := json.MarshalIndent(cacheData, "", " ")
if err != nil {
return
}
_ = os.WriteFile(cachePath, data, 0600)
}
// ExecuteCode executes code and returns a channel that receives the result.
// The channel receives exactly one ExecuteResult then closes.
// This function blocks until completion (polls until job finishes).
func ExecuteCode(creds *Credentials, language, code string) <-chan ExecuteResult {
resultChan := make(chan ExecuteResult, 1)
go func() {
defer close(resultChan)
data := map[string]string{
"language": language,
"code": code,
}
response, err := makeRequest("POST", "/execute", creds, data)
if err != nil {
resultChan <- ExecuteResult{Err: err}
return
}
// If we got a job_id with pending/running status, poll until completion
if jobID, ok := response["job_id"].(string); ok {
if status, ok := response["status"].(string); ok && (status == "pending" || status == "running") {
waitChan := WaitForJob(creds, jobID, 0)
waitResult := <-waitChan
resultChan <- ExecuteResult{Data: waitResult.Data, Err: waitResult.Err}
return
}
}
resultChan <- ExecuteResult{Data: response}
}()
return resultChan
}
// ExecuteAsync executes code asynchronously and returns a channel that receives the job ID.
// The channel receives exactly one JobIDResult then closes.
// This returns immediately with a job_id without waiting for completion.
func ExecuteAsync(creds *Credentials, language, code string) <-chan JobIDResult {
resultChan := make(chan JobIDResult, 1)
go func() {
defer close(resultChan)
data := map[string]string{
"language": language,
"code": code,
}
response, err := makeRequest("POST", "/execute", creds, data)
if err != nil {
resultChan <- JobIDResult{Err: err}
return
}
if jobID, ok := response["job_id"].(string); ok {
resultChan <- JobIDResult{JobID: jobID}
return
}
resultChan <- JobIDResult{Err: fmt.Errorf("no job_id in response")}
}()
return resultChan
}
// GetJob gets current status/result of a job (single poll, no waiting).
// Returns a channel that receives exactly one JobResult then closes.
func GetJob(creds *Credentials, jobID string) <-chan JobResult {
resultChan := make(chan JobResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
resultChan <- JobResult{Data: response, Err: err}
}()
return resultChan
}
// WaitForJob waits for job completion with exponential backoff polling.
// Returns a channel that receives exactly one JobResult then closes.
//
// Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
// Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
//
// timeout: Maximum time to wait (0 = no timeout, wait indefinitely)
func WaitForJob(creds *Credentials, jobID string, timeout time.Duration) <-chan JobResult {
resultChan := make(chan JobResult, 1)
go func() {
defer close(resultChan)
pollCount := 0
var deadline time.Time
if timeout > 0 {
deadline = time.Now().Add(timeout)
}
for {
// Check timeout
if timeout > 0 && time.Now().After(deadline) {
resultChan <- JobResult{Err: fmt.Errorf("timeout waiting for job %s", jobID)}
return
}
// Sleep before polling
delayIdx := pollCount
if delayIdx >= len(PollDelaysMs) {
delayIdx = len(PollDelaysMs) - 1
}
time.Sleep(time.Duration(PollDelaysMs[delayIdx]) * time.Millisecond)
pollCount++
response, err := makeRequest("GET", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
if err != nil {
resultChan <- JobResult{Err: err}
return
}
if status, ok := response["status"].(string); ok {
if status == "completed" || status == "failed" || status == "timeout" || status == "cancelled" {
resultChan <- JobResult{Data: response}
return
}
}
// Still running, continue polling
}
}()
return resultChan
}
// CancelJob cancels a running job.
// Returns a channel that receives exactly one CancelResult then closes.
func CancelJob(creds *Credentials, jobID string) <-chan CancelResult {
resultChan := make(chan CancelResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("DELETE", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
resultChan <- CancelResult{Data: response, Err: err}
}()
return resultChan
}
// ListJobs lists all jobs for the authenticated account.
// Returns a channel that receives exactly one JobListResult then closes.
func ListJobs(creds *Credentials) <-chan JobListResult {
resultChan := make(chan JobListResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", "/jobs", creds, nil)
if err != nil {
resultChan <- JobListResult{Err: err}
return
}
var jobs []map[string]interface{}
if jobsInterface, ok := response["jobs"].([]interface{}); ok {
jobs = make([]map[string]interface{}, len(jobsInterface))
for i, job := range jobsInterface {
if m, ok := job.(map[string]interface{}); ok {
jobs[i] = m
}
}
}
resultChan <- JobListResult{Jobs: jobs}
}()
return resultChan
}
// GetLanguages gets list of supported programming languages.
// Results are cached for 1 hour in ~/.unsandbox/languages.json
// Returns a channel that receives exactly one LanguagesResult then closes.
func GetLanguages(creds *Credentials) <-chan LanguagesResult {
resultChan := make(chan LanguagesResult, 1)
go func() {
defer close(resultChan)
// Try cache first
if cached, ok := loadLanguagesCache(); ok {
resultChan <- LanguagesResult{Languages: cached}
return
}
response, err := makeRequest("GET", "/languages", creds, nil)
if err != nil {
resultChan <- LanguagesResult{Err: err}
return
}
var languages []string
if langs, ok := response["languages"].([]interface{}); ok {
for _, lang := range langs {
if s, ok := lang.(string); ok {
languages = append(languages, s)
}
}
}
// Cache the result
saveLanguagesCache(languages)
resultChan <- LanguagesResult{Languages: languages}
}()
return resultChan
}
// DetectLanguage detects programming language from filename extension.
// This is a synchronous function (no I/O, no goroutines).
//
// Args:
//
// filename: Filename to detect language from (e.g., "script.py")
//
// Returns:
//
// Language identifier (e.g., "python") or empty string if unknown
//
// Examples:
//
// DetectLanguage("hello.py") // -> "python"
// DetectLanguage("script.js") // -> "javascript"
// DetectLanguage("main.go") // -> "go"
// DetectLanguage("unknown") // -> ""
func DetectLanguage(filename string) string {
if !strings.Contains(filename, ".") {
return ""
}
parts := strings.Split(filename, ".")
ext := parts[len(parts)-1]
// Try exact match first (for case-sensitive extensions like .R)
if lang, ok := LanguageMap[ext]; ok {
return lang
}
// Try lowercase match
if lang, ok := LanguageMap[strings.ToLower(ext)]; ok {
return lang
}
return ""
}
// SessionSnapshot creates a snapshot of a session.
// Returns a channel that receives exactly one SnapshotResult then closes.
//
// Args:
//
// creds: API credentials
// sessionID: Session ID to snapshot
// name: Optional snapshot name (empty string for no name)
// hot: If true, creates a hot snapshot of running session
func SessionSnapshot(creds *Credentials, sessionID, name string, hot bool) <-chan SnapshotResult {
resultChan := make(chan SnapshotResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"session_id": sessionID,
"hot": hot,
}
if name != "" {
data["name"] = name
}
response, err := makeRequest("POST", "/snapshots", creds, data)
if err != nil {
resultChan <- SnapshotResult{Err: err}
return
}
if snapshotID, ok := response["snapshot_id"].(string); ok {
resultChan <- SnapshotResult{SnapshotID: snapshotID}
return
}
resultChan <- SnapshotResult{Err: fmt.Errorf("no snapshot_id in response")}
}()
return resultChan
}
// ServiceSnapshot creates a snapshot of a service.
// Returns a channel that receives exactly one SnapshotResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID to snapshot
// name: Optional snapshot name (empty string for no name)
func ServiceSnapshot(creds *Credentials, serviceID, name string) <-chan SnapshotResult {
resultChan := make(chan SnapshotResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"service_id": serviceID,
}
if name != "" {
data["name"] = name
}
response, err := makeRequest("POST", "/snapshots", creds, data)
if err != nil {
resultChan <- SnapshotResult{Err: err}
return
}
if snapshotID, ok := response["snapshot_id"].(string); ok {
resultChan <- SnapshotResult{SnapshotID: snapshotID}
return
}
resultChan <- SnapshotResult{Err: fmt.Errorf("no snapshot_id in response")}
}()
return resultChan
}
// ListSnapshots lists all snapshots.
// Returns a channel that receives exactly one SnapshotListResult then closes.
func ListSnapshots(creds *Credentials) <-chan SnapshotListResult {
resultChan := make(chan SnapshotListResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", "/snapshots", creds, nil)
if err != nil {
resultChan <- SnapshotListResult{Err: err}
return
}
var snapshots []map[string]interface{}
if snapshotsInterface, ok := response["snapshots"].([]interface{}); ok {
snapshots = make([]map[string]interface{}, len(snapshotsInterface))
for i, snapshot := range snapshotsInterface {
if m, ok := snapshot.(map[string]interface{}); ok {
snapshots[i] = m
}
}
}
resultChan <- SnapshotListResult{Snapshots: snapshots}
}()
return resultChan
}
// RestoreSnapshot restores a snapshot.
// Returns a channel that receives exactly one RestoreResult then closes.
func RestoreSnapshot(creds *Credentials, snapshotID string) <-chan RestoreResult {
resultChan := make(chan RestoreResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/restore", snapshotID), creds, map[string]interface{}{})
resultChan <- RestoreResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteSnapshot deletes a snapshot.
// Returns a channel that receives exactly one DeleteResult then closes.
func DeleteSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
resultChan := make(chan DeleteResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("DELETE", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
resultChan <- DeleteResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Session Operations
// ============================================================================
// SessionOptions contains optional parameters for session creation.
type SessionOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use (e.g., "bash", "python3")
TTL int // Time-to-live in seconds (default: 3600)
VCPU int // Number of virtual CPUs (default: 1)
Multiplexer string // Multiplexer to use (e.g., "tmux")
}
// SessionListResult contains the result of listing sessions.
type SessionListResult struct {
Sessions []map[string]interface{}
Err error
}
// SessionResult contains the result of a session operation.
type SessionResult struct {
Data map[string]interface{}
Err error
}
// ListSessions lists all active sessions for the authenticated account.
// Returns a channel that receives exactly one SessionListResult then closes.
func ListSessions(creds *Credentials) <-chan SessionListResult {
resultChan := make(chan SessionListResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", "/sessions", creds, nil)
if err != nil {
resultChan <- SessionListResult{Err: err}
return
}
var sessions []map[string]interface{}
if sessionsInterface, ok := response["sessions"].([]interface{}); ok {
sessions = make([]map[string]interface{}, len(sessionsInterface))
for i, session := range sessionsInterface {
if m, ok := session.(map[string]interface{}); ok {
sessions[i] = m
}
}
}
resultChan <- SessionListResult{Sessions: sessions}
}()
return resultChan
}
// GetSession gets details of a specific session.
// Returns a channel that receives exactly one SessionResult then closes.
func GetSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// CreateSession creates a new interactive session.
// Returns a channel that receives exactly one SessionResult then closes.
//
// Args:
//
// creds: API credentials
// opts: Optional session configuration (can be nil for defaults)
func CreateSession(creds *Credentials, opts *SessionOptions) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
data := make(map[string]interface{})
if opts != nil {
if opts.NetworkMode != "" {
data["network_mode"] = opts.NetworkMode
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.TTL > 0 {
data["ttl"] = opts.TTL
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
if opts.Multiplexer != "" {
data["multiplexer"] = opts.Multiplexer
}
}
response, err := makeRequest("POST", "/sessions", creds, data)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteSession terminates a session.
// Returns a channel that receives exactly one SessionResult then closes.
func DeleteSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("DELETE", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// FreezeSession freezes a session (pauses execution, preserves state).
// Returns a channel that receives exactly one SessionResult then closes.
func FreezeSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/freeze", sessionID), creds, map[string]interface{}{})
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// UnfreezeSession unfreezes a previously frozen session.
// Returns a channel that receives exactly one SessionResult then closes.
func UnfreezeSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/unfreeze", sessionID), creds, map[string]interface{}{})
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// BoostSession increases the vCPU allocation for a session.
// Returns a channel that receives exactly one SessionResult then closes.
//
// Args:
//
// creds: API credentials
// sessionID: Session ID to boost
// vcpu: Number of vCPUs (2, 4, 8, etc.)
func BoostSession(creds *Credentials, sessionID string, vcpu int) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"vcpu": vcpu,
}
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/boost", sessionID), creds, data)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// UnboostSession resets the vCPU allocation for a session to default.
// Returns a channel that receives exactly one SessionResult then closes.
func UnboostSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/unboost", sessionID), creds, map[string]interface{}{})
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// ShellSession executes a command in a session's shell.
// Returns a channel that receives exactly one SessionResult then closes.
//
// Note: For interactive shell access, use the WebSocket-based shell endpoint.
// This function is for executing single commands.
func ShellSession(creds *Credentials, sessionID, command string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"command": command,
}
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/shell", sessionID), creds, data)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Service Operations
// ============================================================================
// ServiceOptions contains optional parameters for service creation.
type ServiceOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use for bootstrap
VCPU int // Number of virtual CPUs
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
}
// ServiceUpdateOptions contains optional parameters for service updates.
type ServiceUpdateOptions struct {
VCPU int // Number of virtual CPUs
}
// ServiceListResult contains the result of listing services.
type ServiceListResult struct {
Services []map[string]interface{}
Err error
}
// ServiceResult contains the result of a service operation.
type ServiceResult struct {
Data map[string]interface{}
Err error
}
// ListServices lists all services for the authenticated account.
// Returns a channel that receives exactly one ServiceListResult then closes.
func ListServices(creds *Credentials) <-chan ServiceListResult {
resultChan := make(chan ServiceListResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", "/services", creds, nil)
if err != nil {
resultChan <- ServiceListResult{Err: err}
return
}
var services []map[string]interface{}
if servicesInterface, ok := response["services"].([]interface{}); ok {
services = make([]map[string]interface{}, len(servicesInterface))
for i, service := range servicesInterface {
if m, ok := service.(map[string]interface{}); ok {
services[i] = m
}
}
}
resultChan <- ServiceListResult{Services: services}
}()
return resultChan
}
// CreateService creates a new persistent service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// name: Service name
// ports: Array of port numbers to expose
// bootstrap: Bootstrap script to run on service start
// opts: Optional service configuration (can be nil for defaults)
func CreateService(creds *Credentials, name string, ports []int, bootstrap string, opts *ServiceOptions) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"name": name,
"ports": ports,
"bootstrap": bootstrap,
}
if opts != nil {
if opts.NetworkMode != "" {
data["network_mode"] = opts.NetworkMode
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
if opts.UnfreezeOnDemand {
data["unfreeze_on_demand"] = true
}
}
response, err := makeRequest("POST", "/services", creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// GetService gets details of a specific service.
// Returns a channel that receives exactly one ServiceResult then closes.
func GetService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/services/%s", serviceID), creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// UpdateService updates a service's configuration.
// Returns a channel that receives exactly one ServiceResult then closes.
func UpdateService(creds *Credentials, serviceID string, opts *ServiceUpdateOptions) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := make(map[string]interface{})
if opts != nil {
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
}
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteService destroys a service.
// Returns a channel that receives exactly one ServiceResult then closes.
func DeleteService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("DELETE", fmt.Sprintf("/services/%s", serviceID), creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// FreezeService freezes a service (pauses execution, preserves state).
// Returns a channel that receives exactly one ServiceResult then closes.
func FreezeService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/freeze", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// UnfreezeService unfreezes a previously frozen service.
// Returns a channel that receives exactly one ServiceResult then closes.
func UnfreezeService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/unfreeze", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// LockService locks a service to prevent modifications or deletion.
// Returns a channel that receives exactly one ServiceResult then closes.
func LockService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/lock", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// UnlockService unlocks a previously locked service.
// Returns a channel that receives exactly one ServiceResult then closes.
func UnlockService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/unlock", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// SetUnfreezeOnDemand enables or disables automatic unfreezing on HTTP request.
// Returns a channel that receives exactly one ServiceResult then closes.
func SetUnfreezeOnDemand(creds *Credentials, serviceID string, enabled bool) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{
"unfreeze_on_demand": enabled,
})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// SetShowFreezePage enables or disables the freeze page display for a service.
// When enabled, frozen services show a branded "waking up" page instead of an error.
// Returns a channel that receives exactly one ServiceResult then closes.
func SetShowFreezePage(creds *Credentials, serviceID string, enabled bool) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{
"show_freeze_page": enabled,
})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// GetServiceLogs retrieves logs from a service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// all: If true, returns all logs; if false, returns only recent logs
func GetServiceLogs(creds *Credentials, serviceID string, all bool) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
path := fmt.Sprintf("/services/%s/logs", serviceID)
if all {
path = fmt.Sprintf("/services/%s/logs?all=true", serviceID)
}
response, err := makeRequest("GET", path, creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// GetServiceEnv retrieves the environment variable names for a service.
// Returns a channel that receives exactly one ServiceResult then closes.
// Note: Values are not returned for security; use ExportServiceEnv for full export.
func GetServiceEnv(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/services/%s/env", serviceID), creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// SetServiceEnv sets environment variables for a service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// env: Map of environment variable names to values
func SetServiceEnv(creds *Credentials, serviceID string, env map[string]string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/env", serviceID), creds, env)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteServiceEnv deletes environment variables from a service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// keys: List of environment variable names to delete (nil deletes all)
func DeleteServiceEnv(creds *Credentials, serviceID string, keys []string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
var data interface{}
if keys != nil {
data = map[string]interface{}{"keys": keys}
}
response, err := makeRequest("DELETE", fmt.Sprintf("/services/%s/env", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// ExportServiceEnv exports all environment variables for a service.
// Returns a channel that receives exactly one ServiceResult then closes.
// Returns the full .env format content with values.
func ExportServiceEnv(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/env/export", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// RedeployService redeploys a service with optional new bootstrap script.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// bootstrap: New bootstrap script (empty string to keep existing)
func RedeployService(creds *Credentials, serviceID string, bootstrap string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := make(map[string]interface{})
if bootstrap != "" {
data["bootstrap"] = bootstrap
}
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// ExecuteInService executes a command in a running service.
// Returns a channel that receives exactly one ServiceResult then closes.
func ExecuteInService(creds *Credentials, serviceID, command string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"command": command,
}
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Additional Snapshot Operations
// ============================================================================
// LockSnapshot locks a snapshot to prevent deletion.
// Returns a channel that receives exactly one DeleteResult then closes.
func LockSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
resultChan := make(chan DeleteResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/lock", snapshotID), creds, map[string]interface{}{})
resultChan <- DeleteResult{Data: response, Err: err}
}()
return resultChan
}
// UnlockSnapshot unlocks a previously locked snapshot.
// Returns a channel that receives exactly one DeleteResult then closes.
func UnlockSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
resultChan := make(chan DeleteResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/unlock", snapshotID), creds, map[string]interface{}{})
resultChan <- DeleteResult{Data: response, Err: err}
}()
return resultChan
}
// CloneSnapshotOptions contains optional parameters for snapshot cloning.
type CloneSnapshotOptions struct {
Name string // Name for the cloned resource
Shell string // Shell to use (for session clones)
Ports []int // Ports to expose (for service clones)
}
// CloneSnapshotResult contains the result of cloning a snapshot.
type CloneSnapshotResult struct {
Data map[string]interface{}
Err error
}
// CloneSnapshot clones a snapshot into a new session or service.
// Returns a channel that receives exactly one CloneSnapshotResult then closes.
//
// Args:
//
// creds: API credentials
// snapshotID: Snapshot ID to clone
// cloneType: "session" or "service"
// opts: Optional clone configuration (can be nil)
func CloneSnapshot(creds *Credentials, snapshotID, cloneType string, opts *CloneSnapshotOptions) <-chan CloneSnapshotResult {
resultChan := make(chan CloneSnapshotResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"type": cloneType,
}
if opts != nil {
if opts.Name != "" {
data["name"] = opts.Name
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.Ports != nil {
data["ports"] = opts.Ports
}
}
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/clone", snapshotID), creds, data)
resultChan <- CloneSnapshotResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Key Validation
// ============================================================================
// ValidateKeysResult contains the result of validating API keys.
type ValidateKeysResult struct {
Data map[string]interface{}
Err error
}
// ValidateKeys validates the API credentials with the server.
// Returns a channel that receives exactly one ValidateKeysResult then closes.
// Returns account information if valid, error if invalid.
func ValidateKeys(creds *Credentials) <-chan ValidateKeysResult {
resultChan := make(chan ValidateKeysResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", "/keys/validate", creds, map[string]interface{}{})
resultChan <- ValidateKeysResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Image Generation
// ============================================================================
// ImageOptions contains options for image generation
type ImageOptions struct {
Model string
Size string // default "1024x1024"
Quality string // "standard" or "hd"
N int // number of images
}
// ImageResult contains the generated images
type ImageResult struct {
Images []string
CreatedAt string
Err error
}
// Image generates images from a text prompt using AI.
// Returns a channel that receives exactly one ImageResult then closes.
func Image(creds *Credentials, prompt string, opts *ImageOptions) <-chan ImageResult {
resultChan := make(chan ImageResult, 1)
go func() {
defer close(resultChan)
if opts == nil {
opts = &ImageOptions{}
}
if opts.Size == "" {
opts.Size = "1024x1024"
}
if opts.Quality == "" {
opts.Quality = "standard"
}
if opts.N == 0 {
opts.N = 1
}
payload := map[string]interface{}{
"prompt": prompt,
"size": opts.Size,
"quality": opts.Quality,
"n": opts.N,
}
if opts.Model != "" {
payload["model"] = opts.Model
}
response, err := makeRequest("POST", "/image", creds, payload)
if err != nil {
resultChan <- ImageResult{Err: err}
return
}
// Extract images array from response
var images []string
if imagesInterface, ok := response["images"].([]interface{}); ok {
for _, img := range imagesInterface {
if s, ok := img.(string); ok {
images = append(images, s)
}
}
}
createdAt := ""
if ca, ok := response["created_at"].(string); ok {
createdAt = ca
}
resultChan <- ImageResult{
Images: images,
CreatedAt: createdAt,
}
}()
return resultChan
}
// ============================================================================
// Snapshot Info
// ============================================================================
// GetSnapshotResult contains the result of getting a snapshot
type GetSnapshotResult struct {
Data map[string]interface{}
Err error
}
// GetSnapshot gets details of a specific snapshot.
// Returns a channel that receives exactly one GetSnapshotResult then closes.
func GetSnapshot(creds *Credentials, snapshotID string) <-chan GetSnapshotResult {
resultChan := make(chan GetSnapshotResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
resultChan <- GetSnapshotResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// CLI Implementation
// ============================================================================
// Exit codes
const (
ExitSuccess = 0
ExitGeneralError = 1
ExitInvalidArgs = 2
ExitAuthError = 3
ExitAPIError = 4
ExitTimeout = 5
)
// CLIOptions holds parsed CLI arguments
type CLIOptions struct {
// Global options
Shell string
Env []string
Files []string
FilePaths []string
Artifacts bool
OutputDir string
PublicKey string
SecretKey string
Network string
VCPU int
Yes bool
Help bool
// Command
Command string
SubCommand string
Args []string
}
// printUsage prints the main help message
func printUsage() {
fmt.Fprintf(os.Stderr, `unsandbox - Execute code securely in sandboxed containers (async)
Usage:
un [options] <source_file> Execute code file
un [options] -s LANG 'code' Execute inline code
un session [options] Interactive session
un service [options] Manage services
un snapshot [options] Manage snapshots
un 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: zerotrust (default) or semitrusted
-v, --vcpu N vCPU count (1-8)
-y, --yes Skip confirmation prompts
-h, --help Show help
Examples:
un script.py Execute Python script
un -s bash 'echo hello' Inline bash command
un -e DEBUG=1 script.py With environment variable
un -n semitrusted crawler.py With network access
un session --tmux Persistent interactive session
un service --list List all services
`)
}
// printSessionUsage prints session subcommand help
func printSessionUsage() {
fmt.Fprintf(os.Stderr, `un session - Interactive session management
Usage:
un session [options] Start new interactive session
un session --list List active sessions
un session --attach ID Reconnect to session
un session --kill ID Terminate session
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
-n, --network MODE Network: zerotrust or semitrusted
-v, --vcpu N vCPU count (1-8)
Examples:
un session Interactive bash
un session --shell python3 Python REPL
un session --tmux Persistent session
un session --list List sessions
un session --attach abc123 Reconnect to session
`)
}
// printServiceUsage prints service subcommand help
func printServiceUsage() {
fmt.Fprintf(os.Stderr, `un service - Service management
Usage:
un service --list List all services
un service --name NAME --ports PORTS --bootstrap CMD
un service --info ID Get service details
un service --logs ID Get bootstrap logs
un service env [status|set|export|delete] 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
--snapshot ID Create snapshot
Service Environment Vault:
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
Examples:
un service --list
un service --name web --ports 80 --bootstrap "python -m http.server 80"
un service --info abc123
un service --execute abc123 'ls -la'
un service env set abc123 --env-file .env
`)
}
// printSnapshotUsage prints snapshot subcommand help
func printSnapshotUsage() {
fmt.Fprintf(os.Stderr, `un snapshot - Snapshot management
Usage:
un snapshot --list List all snapshots
un snapshot --info ID Get details
un snapshot --delete ID Delete snapshot
un snapshot --clone ID Clone to new session/service
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 service
--shell SHELL Shell for cloned session
--ports PORTS Ports for cloned service
Examples:
un snapshot --list
un snapshot --info abc123
un snapshot --delete abc123
un snapshot --clone abc123 --type service --name myapp --ports 80
`)
}
// cliError prints error to stderr and returns exit code
func cliError(msg string, code int) int {
fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
return code
}
// parsePorts parses comma-separated port numbers
func parsePorts(s string) ([]int, error) {
if s == "" {
return nil, nil
}
parts := strings.Split(s, ",")
ports := make([]int, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
port, err := strconv.Atoi(p)
if err != nil {
return nil, fmt.Errorf("invalid port: %s", p)
}
ports = append(ports, port)
}
return ports, nil
}
// formatList formats a list of items for display
func formatList(items []map[string]interface{}, fields []string) {
if len(items) == 0 {
fmt.Println("No items found.")
return
}
// Print header
header := strings.Join(fields, "\t")
fmt.Println(header)
// Print items
for _, item := range items {
values := make([]string, len(fields))
for i, field := range fields {
if val, ok := item[field]; ok {
values[i] = fmt.Sprintf("%v", val)
} else {
values[i] = "-"
}
}
fmt.Println(strings.Join(values, "\t"))
}
}
// readFileContents reads file contents for bootstrapping
func readFileContents(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
// readEnvFile reads environment variables from a .env file
func readEnvFile(path string) (map[string]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
env := make(map[string]string)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Remove surrounding quotes if present
if len(value) >= 2 && (value[0] == '"' && value[len(value)-1] == '"' ||
value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
env[key] = value
}
}
return env, nil
}
// runExecute handles the execute command (default)
func runExecute(creds *Credentials, opts *CLIOptions) int {
var language, code string
if opts.Shell != "" {
// Inline code mode: -s LANG 'code'
language = opts.Shell
if len(opts.Args) == 0 {
return cliError("no code provided for inline execution", ExitInvalidArgs)
}
code = opts.Args[0]
} else {
// File mode
if len(opts.Args) == 0 {
return cliError("no source file provided", ExitInvalidArgs)
}
filename := opts.Args[0]
language = DetectLanguage(filename)
if language == "" {
return cliError(fmt.Sprintf("cannot detect language for: %s", filename), ExitInvalidArgs)
}
fileContent, err := readFileContents(filename)
if err != nil {
return cliError(fmt.Sprintf("cannot read file: %s", err), ExitGeneralError)
}
code = fileContent
}
// Build request data
data := map[string]interface{}{
"language": language,
"code": code,
}
if opts.Network != "" {
data["network_mode"] = opts.Network
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
if len(opts.Env) > 0 {
envMap := make(map[string]string)
for _, e := range opts.Env {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
data["env"] = envMap
}
// Execute asynchronously and wait for result
resultChan := ExecuteCode(creds, language, code)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
// Print output
if stdout, ok := result.Data["stdout"].(string); ok && stdout != "" {
fmt.Print(stdout)
}
if stderr, ok := result.Data["stderr"].(string); ok && stderr != "" {
fmt.Fprint(os.Stderr, stderr)
}
// Print summary
fmt.Println("---")
if exitCode, ok := result.Data["exit_code"].(float64); ok {
fmt.Printf("Exit code: %d\n", int(exitCode))
}
if execTime, ok := result.Data["execution_time_ms"].(float64); ok {
fmt.Printf("Execution time: %dms\n", int(execTime))
}
if exitCode, ok := result.Data["exit_code"].(float64); ok && exitCode != 0 {
return int(exitCode)
}
return ExitSuccess
}
// runSession handles the session command
func runSession(creds *Credentials, args []string, opts *CLIOptions) int {
fs := &sessionFlags{}
parseSessionFlags(args, fs)
if fs.help {
printSessionUsage()
return ExitSuccess
}
// List sessions
if fs.list {
resultChan := ListSessions(creds)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
formatList(result.Sessions, []string{"session_id", "status", "shell", "created_at"})
return ExitSuccess
}
// Attach to session
if fs.attach != "" {
resultChan := GetSession(creds, fs.attach)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
// Print connection info
fmt.Printf("Session: %s\n", fs.attach)
if wsURL, ok := result.Data["websocket_url"].(string); ok {
fmt.Printf("Connect via: %s\n", wsURL)
}
return ExitSuccess
}
// Kill session
if fs.kill != "" {
resultChan := DeleteSession(creds, fs.kill)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Session %s terminated\n", fs.kill)
return ExitSuccess
}
// Freeze session
if fs.freeze != "" {
resultChan := FreezeSession(creds, fs.freeze)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Session %s frozen\n", fs.freeze)
return ExitSuccess
}
// Unfreeze session
if fs.unfreeze != "" {
resultChan := UnfreezeSession(creds, fs.unfreeze)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Session %s unfrozen\n", fs.unfreeze)
return ExitSuccess
}
// Boost session
if fs.boost != "" {
vcpu := opts.VCPU
if vcpu == 0 {
vcpu = 2
}
resultChan := BoostSession(creds, fs.boost, vcpu)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Session %s boosted to %d vCPUs\n", fs.boost, vcpu)
return ExitSuccess
}
// Unboost session
if fs.unboost != "" {
resultChan := UnboostSession(creds, fs.unboost)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Session %s unboost\n", fs.unboost)
return ExitSuccess
}
// Snapshot session
if fs.snapshot != "" {
resultChan := SessionSnapshot(creds, fs.snapshot, fs.snapshotName, fs.hot)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Snapshot created: %s\n", result.SnapshotID)
return ExitSuccess
}
// Create new session
sessionOpts := &SessionOptions{}
if fs.shell != "" {
sessionOpts.Shell = fs.shell
}
if opts.Network != "" {
sessionOpts.NetworkMode = opts.Network
}
if opts.VCPU > 0 {
sessionOpts.VCPU = opts.VCPU
}
if fs.tmux {
sessionOpts.Multiplexer = "tmux"
} else if fs.screen {
sessionOpts.Multiplexer = "screen"
}
resultChan := CreateSession(creds, sessionOpts)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Session created: %v\n", result.Data["session_id"])
if wsURL, ok := result.Data["websocket_url"].(string); ok {
fmt.Printf("Connect via: %s\n", wsURL)
}
return ExitSuccess
}
type sessionFlags struct {
shell string
list bool
attach string
kill string
freeze string
unfreeze string
boost string
unboost string
tmux bool
screen bool
snapshot string
snapshotName string
hot bool
audit bool
help bool
}
func parseSessionFlags(args []string, fs *sessionFlags) {
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "--shell":
if i+1 < len(args) {
fs.shell = args[i+1]
i++
}
case "-l", "--list":
fs.list = true
case "--attach":
if i+1 < len(args) {
fs.attach = args[i+1]
i++
}
case "--kill":
if i+1 < len(args) {
fs.kill = args[i+1]
i++
}
case "--freeze":
if i+1 < len(args) {
fs.freeze = args[i+1]
i++
}
case "--unfreeze":
if i+1 < len(args) {
fs.unfreeze = args[i+1]
i++
}
case "--boost":
if i+1 < len(args) {
fs.boost = args[i+1]
i++
}
case "--unboost":
if i+1 < len(args) {
fs.unboost = args[i+1]
i++
}
case "--tmux":
fs.tmux = true
case "--screen":
fs.screen = true
case "--snapshot":
if i+1 < len(args) {
fs.snapshot = args[i+1]
i++
}
case "--snapshot-name":
if i+1 < len(args) {
fs.snapshotName = args[i+1]
i++
}
case "--hot":
fs.hot = true
case "--audit":
fs.audit = true
case "-h", "--help":
fs.help = true
}
}
}
// runService handles the service command
func runService(creds *Credentials, args []string, opts *CLIOptions) int {
fs := &serviceFlags{}
parseServiceFlags(args, fs)
if fs.help {
printServiceUsage()
return ExitSuccess
}
// Handle env subcommand
if fs.envCmd != "" {
return runServiceEnv(creds, fs, opts)
}
// List services
if fs.list {
resultChan := ListServices(creds)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
formatList(result.Services, []string{"service_id", "name", "status", "created_at"})
return ExitSuccess
}
// Get service info
if fs.info != "" {
resultChan := GetService(creds, fs.info)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
fmt.Println(string(jsonOut))
return ExitSuccess
}
// Get service logs
if fs.logs != "" {
resultChan := GetServiceLogs(creds, fs.logs, true)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
if content, ok := result.Data["logs"].(string); ok {
fmt.Print(content)
}
return ExitSuccess
}
// Get service tail logs
if fs.tail != "" {
resultChan := GetServiceLogs(creds, fs.tail, false)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
if content, ok := result.Data["logs"].(string); ok {
fmt.Print(content)
}
return ExitSuccess
}
// Freeze service
if fs.freeze != "" {
resultChan := FreezeService(creds, fs.freeze)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s frozen\n", fs.freeze)
return ExitSuccess
}
// Unfreeze service
if fs.unfreeze != "" {
resultChan := UnfreezeService(creds, fs.unfreeze)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s unfrozen\n", fs.unfreeze)
return ExitSuccess
}
// Destroy service
if fs.destroy != "" {
resultChan := DeleteService(creds, fs.destroy)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s destroyed\n", fs.destroy)
return ExitSuccess
}
// Lock service
if fs.lock != "" {
resultChan := LockService(creds, fs.lock)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s locked\n", fs.lock)
return ExitSuccess
}
// Unlock service
if fs.unlock != "" {
resultChan := UnlockService(creds, fs.unlock)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s unlocked\n", fs.unlock)
return ExitSuccess
}
// Resize service
if fs.resize != "" {
if opts.VCPU == 0 {
return cliError("--vcpu required for resize", ExitInvalidArgs)
}
resultChan := UpdateService(creds, fs.resize, &ServiceUpdateOptions{VCPU: opts.VCPU})
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s resized to %d vCPUs\n", fs.resize, opts.VCPU)
return ExitSuccess
}
// Redeploy service
if fs.redeploy != "" {
resultChan := RedeployService(creds, fs.redeploy, fs.bootstrap)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service %s redeployed\n", fs.redeploy)
return ExitSuccess
}
// Execute in service
if fs.execute != "" && fs.executeCmd != "" {
resultChan := ExecuteInService(creds, fs.execute, fs.executeCmd)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
if stdout, ok := result.Data["stdout"].(string); ok {
fmt.Print(stdout)
}
if stderr, ok := result.Data["stderr"].(string); ok {
fmt.Fprint(os.Stderr, stderr)
}
return ExitSuccess
}
// Snapshot service
if fs.snapshot != "" {
resultChan := ServiceSnapshot(creds, fs.snapshot, fs.snapshotName)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Snapshot created: %s\n", result.SnapshotID)
return ExitSuccess
}
// Create new service
if fs.name != "" {
if fs.ports == "" {
return cliError("--ports required for new service", ExitInvalidArgs)
}
ports, err := parsePorts(fs.ports)
if err != nil {
return cliError(err.Error(), ExitInvalidArgs)
}
bootstrap := fs.bootstrap
if fs.bootstrapFile != "" {
content, err := readFileContents(fs.bootstrapFile)
if err != nil {
return cliError(fmt.Sprintf("cannot read bootstrap file: %s", err), ExitGeneralError)
}
bootstrap = content
}
serviceOpts := &ServiceOptions{}
if opts.Network != "" {
serviceOpts.NetworkMode = opts.Network
}
if opts.VCPU > 0 {
serviceOpts.VCPU = opts.VCPU
}
resultChan := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Service created: %v\n", result.Data["service_id"])
if url, ok := result.Data["url"].(string); ok {
fmt.Printf("URL: %s\n", url)
}
return ExitSuccess
}
printServiceUsage()
return ExitInvalidArgs
}
type serviceFlags struct {
name string
ports string
domains string
serviceType string
bootstrap string
bootstrapFile string
envFile string
list bool
info string
logs string
tail string
freeze string
unfreeze string
destroy string
lock string
unlock string
resize string
redeploy string
execute string
executeCmd string
snapshot string
snapshotName string
hot bool
envCmd string
envID string
help bool
}
func parseServiceFlags(args []string, fs *serviceFlags) {
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "--name":
if i+1 < len(args) {
fs.name = args[i+1]
i++
}
case "--ports":
if i+1 < len(args) {
fs.ports = args[i+1]
i++
}
case "--domains":
if i+1 < len(args) {
fs.domains = args[i+1]
i++
}
case "--type":
if i+1 < len(args) {
fs.serviceType = args[i+1]
i++
}
case "--bootstrap":
if i+1 < len(args) {
fs.bootstrap = args[i+1]
i++
}
case "--bootstrap-file":
if i+1 < len(args) {
fs.bootstrapFile = args[i+1]
i++
}
case "--env-file":
if i+1 < len(args) {
fs.envFile = args[i+1]
i++
}
case "-l", "--list":
fs.list = true
case "--info":
if i+1 < len(args) {
fs.info = args[i+1]
i++
}
case "--logs":
if i+1 < len(args) {
fs.logs = args[i+1]
i++
}
case "--tail":
if i+1 < len(args) {
fs.tail = args[i+1]
i++
}
case "--freeze":
if i+1 < len(args) {
fs.freeze = args[i+1]
i++
}
case "--unfreeze":
if i+1 < len(args) {
fs.unfreeze = args[i+1]
i++
}
case "--destroy":
if i+1 < len(args) {
fs.destroy = args[i+1]
i++
}
case "--lock":
if i+1 < len(args) {
fs.lock = args[i+1]
i++
}
case "--unlock":
if i+1 < len(args) {
fs.unlock = args[i+1]
i++
}
case "--resize":
if i+1 < len(args) {
fs.resize = args[i+1]
i++
}
case "--redeploy":
if i+1 < len(args) {
fs.redeploy = args[i+1]
i++
}
case "--execute":
if i+1 < len(args) {
fs.execute = args[i+1]
i++
// Next arg is the command
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
fs.executeCmd = args[i+1]
i++
}
}
case "--snapshot":
if i+1 < len(args) {
fs.snapshot = args[i+1]
i++
}
case "--snapshot-name":
if i+1 < len(args) {
fs.snapshotName = args[i+1]
i++
}
case "--hot":
fs.hot = true
case "env":
// Handle env subcommand
if i+1 < len(args) {
fs.envCmd = args[i+1]
i++
if i+1 < len(args) {
fs.envID = args[i+1]
i++
}
}
case "-h", "--help":
fs.help = true
}
}
}
// runServiceEnv handles service env subcommands
func runServiceEnv(creds *Credentials, fs *serviceFlags, opts *CLIOptions) int {
if fs.envID == "" {
return cliError("service ID required for env command", ExitInvalidArgs)
}
switch fs.envCmd {
case "status":
resultChan := GetServiceEnv(creds, fs.envID)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
fmt.Println(string(jsonOut))
case "set":
var env map[string]string
var err error
if fs.envFile != "" {
env, err = readEnvFile(fs.envFile)
if err != nil {
return cliError(fmt.Sprintf("cannot read env file: %s", err), ExitGeneralError)
}
} else {
// Read from stdin
data, err := io.ReadAll(os.Stdin)
if err != nil {
return cliError(fmt.Sprintf("cannot read stdin: %s", err), ExitGeneralError)
}
env = make(map[string]string)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
env[parts[0]] = parts[1]
}
}
}
resultChan := SetServiceEnv(creds, fs.envID, env)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Println("Environment variables set")
case "export":
resultChan := ExportServiceEnv(creds, fs.envID)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
if content, ok := result.Data["content"].(string); ok {
fmt.Print(content)
}
case "delete":
resultChan := DeleteServiceEnv(creds, fs.envID, nil)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Println("Environment vault deleted")
default:
return cliError(fmt.Sprintf("unknown env command: %s", fs.envCmd), ExitInvalidArgs)
}
return ExitSuccess
}
// runSnapshot handles the snapshot command
func runSnapshot(creds *Credentials, args []string, opts *CLIOptions) int {
fs := &snapshotFlags{}
parseSnapshotFlags(args, fs)
if fs.help {
printSnapshotUsage()
return ExitSuccess
}
// List snapshots
if fs.list {
resultChan := ListSnapshots(creds)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
formatList(result.Snapshots, []string{"snapshot_id", "name", "type", "created_at"})
return ExitSuccess
}
// Get snapshot info
if fs.info != "" {
resultChan := GetSnapshot(creds, fs.info)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
fmt.Println(string(jsonOut))
return ExitSuccess
}
// Delete snapshot
if fs.deleteID != "" {
resultChan := DeleteSnapshot(creds, fs.deleteID)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Snapshot %s deleted\n", fs.deleteID)
return ExitSuccess
}
// Lock snapshot
if fs.lock != "" {
resultChan := LockSnapshot(creds, fs.lock)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Snapshot %s locked\n", fs.lock)
return ExitSuccess
}
// Unlock snapshot
if fs.unlock != "" {
resultChan := UnlockSnapshot(creds, fs.unlock)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
fmt.Printf("Snapshot %s unlocked\n", fs.unlock)
return ExitSuccess
}
// Clone snapshot
if fs.clone != "" {
cloneOpts := &CloneSnapshotOptions{}
if fs.name != "" {
cloneOpts.Name = fs.name
}
if fs.shell != "" {
cloneOpts.Shell = fs.shell
}
if fs.ports != "" {
ports, err := parsePorts(fs.ports)
if err != nil {
return cliError(err.Error(), ExitInvalidArgs)
}
cloneOpts.Ports = ports
}
cloneType := fs.cloneType
if cloneType == "" {
cloneType = "session"
}
resultChan := CloneSnapshot(creds, fs.clone, cloneType, cloneOpts)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAPIError)
}
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
fmt.Println(string(jsonOut))
return ExitSuccess
}
printSnapshotUsage()
return ExitInvalidArgs
}
type snapshotFlags struct {
list bool
info string
deleteID string
lock string
unlock string
clone string
cloneType string
name string
shell string
ports string
help bool
}
func parseSnapshotFlags(args []string, fs *snapshotFlags) {
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "-l", "--list":
fs.list = true
case "--info":
if i+1 < len(args) {
fs.info = args[i+1]
i++
}
case "--delete":
if i+1 < len(args) {
fs.deleteID = args[i+1]
i++
}
case "--lock":
if i+1 < len(args) {
fs.lock = args[i+1]
i++
}
case "--unlock":
if i+1 < len(args) {
fs.unlock = args[i+1]
i++
}
case "--clone":
if i+1 < len(args) {
fs.clone = args[i+1]
i++
}
case "--type":
if i+1 < len(args) {
fs.cloneType = args[i+1]
i++
}
case "--name":
if i+1 < len(args) {
fs.name = args[i+1]
i++
}
case "--shell":
if i+1 < len(args) {
fs.shell = args[i+1]
i++
}
case "--ports":
if i+1 < len(args) {
fs.ports = args[i+1]
i++
}
case "-h", "--help":
fs.help = true
}
}
}
// runKey handles the key command
func runKey(creds *Credentials) int {
resultChan := ValidateKeys(creds)
result := <-resultChan
if result.Err != nil {
return cliError(result.Err.Error(), ExitAuthError)
}
fmt.Println("API key is valid")
if account, ok := result.Data["account"].(map[string]interface{}); ok {
jsonOut, _ := json.MarshalIndent(account, "", " ")
fmt.Println(string(jsonOut))
}
return ExitSuccess
}
// parseGlobalFlags parses global CLI options
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
opts := &CLIOptions{}
remaining := []string{}
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "-s" || arg == "--shell":
if i+1 < len(args) {
opts.Shell = args[i+1]
i++
}
case arg == "-e" || arg == "--env":
if i+1 < len(args) {
opts.Env = append(opts.Env, args[i+1])
i++
}
case arg == "-f" || arg == "--file":
if i+1 < len(args) {
opts.Files = append(opts.Files, args[i+1])
i++
}
case arg == "-F" || arg == "--file-path":
if i+1 < len(args) {
opts.FilePaths = append(opts.FilePaths, args[i+1])
i++
}
case arg == "-a" || arg == "--artifacts":
opts.Artifacts = true
case arg == "-o" || arg == "--output":
if i+1 < len(args) {
opts.OutputDir = args[i+1]
i++
}
case arg == "-p" || arg == "--public-key":
if i+1 < len(args) {
opts.PublicKey = args[i+1]
i++
}
case arg == "-k" || arg == "--secret-key":
if i+1 < len(args) {
opts.SecretKey = args[i+1]
i++
}
case arg == "-n" || arg == "--network":
if i+1 < len(args) {
opts.Network = args[i+1]
i++
}
case arg == "-v" || arg == "--vcpu":
if i+1 < len(args) {
if v, err := strconv.Atoi(args[i+1]); err == nil {
opts.VCPU = v
}
i++
}
case arg == "-y" || arg == "--yes":
opts.Yes = true
case arg == "-h" || arg == "--help":
opts.Help = true
default:
remaining = append(remaining, arg)
}
}
return opts, remaining
}
// CliMain is the main entry point for the CLI
func CliMain() {
if len(os.Args) < 2 {
printUsage()
os.Exit(ExitInvalidArgs)
}
opts, remaining := parseGlobalFlags(os.Args[1:])
if opts.Help && len(remaining) == 0 {
printUsage()
os.Exit(ExitSuccess)
}
// Determine command
command := ""
cmdArgs := remaining
if len(remaining) > 0 {
switch remaining[0] {
case "session", "service", "snapshot", "key":
command = remaining[0]
cmdArgs = remaining[1:]
default:
command = "execute"
opts.Args = remaining
}
}
// Resolve credentials
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(ExitAuthError)
}
// Dispatch command
var exitCode int
switch command {
case "execute", "":
if opts.Help {
printUsage()
exitCode = ExitSuccess
} else {
exitCode = runExecute(creds, opts)
}
case "session":
exitCode = runSession(creds, cmdArgs, opts)
case "service":
exitCode = runService(creds, cmdArgs, opts)
case "snapshot":
exitCode = runSnapshot(creds, cmdArgs, opts)
case "key":
exitCode = runKey(creds)
default:
printUsage()
exitCode = ExitInvalidArgs
}
os.Exit(exitCode)
}