684 lines
21 KiB
Ruby
684 lines
21 KiB
Ruby
# 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
|
|
|
|
#!/usr/bin/env ruby
|
|
# un.rb - Unsandbox CLI Client (Ruby Implementation)
|
|
#
|
|
# Full-featured CLI matching un.c capabilities:
|
|
# - Execute code with env vars, input files, artifacts
|
|
# - Interactive sessions with shell/REPL support
|
|
# - Persistent services with domains and ports
|
|
#
|
|
# Usage:
|
|
# un.rb [options] <source_file>
|
|
# un.rb session [options]
|
|
# un.rb service [options]
|
|
#
|
|
# Requires: UNSANDBOX_API_KEY environment variable
|
|
|
|
require 'json'
|
|
require 'net/http'
|
|
require 'uri'
|
|
require 'base64'
|
|
require 'fileutils'
|
|
require 'optparse'
|
|
|
|
API_BASE = 'https://api.unsandbox.com'
|
|
PORTAL_BASE = 'https://unsandbox.com'
|
|
BLUE = "\e[34m"
|
|
RED = "\e[31m"
|
|
GREEN = "\e[32m"
|
|
YELLOW = "\e[33m"
|
|
RESET = "\e[0m"
|
|
|
|
EXT_MAP = {
|
|
'.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' => 'v',
|
|
'.dart' => 'dart', '.groovy' => 'groovy', '.scala' => 'scala',
|
|
'.f90' => 'fortran', '.f95' => 'fortran', '.cob' => 'cobol',
|
|
'.pro' => 'prolog', '.forth' => 'forth', '.4th' => 'forth',
|
|
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
|
|
}.freeze
|
|
|
|
def get_api_keys(args_key = nil)
|
|
public_key = ENV['UNSANDBOX_PUBLIC_KEY']
|
|
secret_key = ENV['UNSANDBOX_SECRET_KEY']
|
|
|
|
unless public_key && secret_key
|
|
old_key = args_key || ENV['UNSANDBOX_API_KEY']
|
|
if old_key
|
|
public_key = old_key
|
|
secret_key = old_key
|
|
else
|
|
warn "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
|
|
warn "#{RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)#{RESET}"
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
{ public_key: public_key, secret_key: secret_key }
|
|
end
|
|
|
|
def detect_language(filename)
|
|
ext = File.extname(filename).downcase
|
|
lang = EXT_MAP[ext]
|
|
unless lang
|
|
begin
|
|
first_line = File.open(filename, &:readline)
|
|
if first_line.start_with?('#!')
|
|
return 'python' if first_line.include?('python')
|
|
return 'javascript' if first_line.include?('node')
|
|
return 'ruby' if first_line.include?('ruby')
|
|
return 'perl' if first_line.include?('perl')
|
|
return 'bash' if first_line.include?('bash') || first_line.include?('/sh')
|
|
return 'lua' if first_line.include?('lua')
|
|
return 'php' if first_line.include?('php')
|
|
end
|
|
rescue
|
|
end
|
|
warn "#{RED}Error: Cannot detect language for #{filename}#{RESET}"
|
|
exit 1
|
|
end
|
|
lang
|
|
end
|
|
|
|
def api_request(endpoint, method: 'GET', data: nil, keys:)
|
|
require 'openssl'
|
|
|
|
uri = URI("#{API_BASE}#{endpoint}")
|
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
http.use_ssl = true
|
|
http.read_timeout = 300
|
|
|
|
timestamp = Time.now.to_i.to_s
|
|
body = data ? JSON.generate(data) : ''
|
|
message = "#{timestamp}:#{method}:#{uri.path}#{uri.query ? "?#{uri.query}" : ''}:#{body}"
|
|
signature = OpenSSL::HMAC.hexdigest('SHA256', keys[:secret_key], message)
|
|
|
|
request = case method
|
|
when 'GET' then Net::HTTP::Get.new(uri)
|
|
when 'POST' then Net::HTTP::Post.new(uri)
|
|
when 'DELETE' then Net::HTTP::Delete.new(uri)
|
|
else raise "Unknown method: #{method}"
|
|
end
|
|
|
|
request['Authorization'] = "Bearer #{keys[:public_key]}"
|
|
request['X-Timestamp'] = timestamp
|
|
request['X-Signature'] = signature
|
|
request['Content-Type'] = 'application/json'
|
|
request.body = body if data
|
|
|
|
response = http.request(request)
|
|
unless response.is_a?(Net::HTTPSuccess)
|
|
if response.code == '401' && response.body.downcase.include?('timestamp')
|
|
warn "#{RED}Error: Request timestamp expired (must be within 5 minutes of server time)#{RESET}"
|
|
warn "#{YELLOW}Your computer's clock may have drifted.#{RESET}"
|
|
warn "#{YELLOW}Check your system time and sync with NTP if needed:#{RESET}"
|
|
warn " Linux: sudo ntpdate -s time.nist.gov"
|
|
warn " macOS: sudo sntp -sS time.apple.com"
|
|
warn " Windows: w32tm /resync"
|
|
else
|
|
warn "#{RED}Error: HTTP #{response.code} - #{response.body}#{RESET}"
|
|
end
|
|
exit 1
|
|
end
|
|
|
|
JSON.parse(response.body)
|
|
rescue => e
|
|
warn "#{RED}Error: #{e.message}#{RESET}"
|
|
exit 1
|
|
end
|
|
|
|
def cmd_execute(options)
|
|
keys = get_api_keys(options[:api_key])
|
|
|
|
unless File.exist?(options[:source_file])
|
|
warn "#{RED}Error: File not found: #{options[:source_file]}#{RESET}"
|
|
exit 1
|
|
end
|
|
|
|
code = File.read(options[:source_file])
|
|
language = detect_language(options[:source_file])
|
|
|
|
payload = { language: language, code: code }
|
|
|
|
if options[:env] && !options[:env].empty?
|
|
env_vars = {}
|
|
options[:env].each do |e|
|
|
k, v = e.split('=', 2)
|
|
env_vars[k] = v if k && v
|
|
end
|
|
payload[:env] = env_vars unless env_vars.empty?
|
|
end
|
|
|
|
if options[:files] && !options[:files].empty?
|
|
input_files = options[:files].map do |filepath|
|
|
unless File.exist?(filepath)
|
|
warn "#{RED}Error: Input file not found: #{filepath}#{RESET}"
|
|
exit 1
|
|
end
|
|
{
|
|
filename: File.basename(filepath),
|
|
content_base64: Base64.strict_encode64(File.read(filepath, mode: 'rb'))
|
|
}
|
|
end
|
|
payload[:input_files] = input_files
|
|
end
|
|
|
|
payload[:return_artifacts] = true if options[:artifacts]
|
|
payload[:network] = options[:network] if options[:network]
|
|
payload[:vcpu] = options[:vcpu] if options[:vcpu]
|
|
|
|
result = api_request('/execute', method: 'POST', data: payload, keys: keys)
|
|
|
|
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
|
|
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
|
|
|
|
if options[:artifacts] && result['artifacts']
|
|
out_dir = options[:output_dir] || '.'
|
|
FileUtils.mkdir_p(out_dir) unless Dir.exist?(out_dir)
|
|
result['artifacts'].each do |artifact|
|
|
filename = artifact['filename'] || 'artifact'
|
|
content = Base64.strict_decode64(artifact['content_base64'])
|
|
filepath = File.join(out_dir, filename)
|
|
File.write(filepath, content, mode: 'wb')
|
|
File.chmod(0755, filepath)
|
|
warn "#{GREEN}Saved: #{filepath}#{RESET}"
|
|
end
|
|
end
|
|
|
|
exit(result['exit_code'] || 0)
|
|
end
|
|
|
|
def cmd_session(options)
|
|
keys = get_api_keys(options[:api_key])
|
|
|
|
if options[:list]
|
|
result = api_request('/sessions', keys: keys)
|
|
sessions = result['sessions'] || []
|
|
if sessions.empty?
|
|
puts 'No active sessions'
|
|
else
|
|
puts format('%-40s %-10s %-10s %s', 'ID', 'Shell', 'Status', 'Created')
|
|
sessions.each do |s|
|
|
puts format('%-40s %-10s %-10s %s',
|
|
s['id'] || 'N/A', s['shell'] || 'N/A',
|
|
s['status'] || 'N/A', s['created_at'] || 'N/A')
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if options[:kill]
|
|
api_request("/sessions/#{options[:kill]}", method: 'DELETE', keys: keys)
|
|
puts "#{GREEN}Session terminated: #{options[:kill]}#{RESET}"
|
|
return
|
|
end
|
|
|
|
if options[:attach]
|
|
puts "#{YELLOW}Attaching to session #{options[:attach]}...#{RESET}"
|
|
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
|
return
|
|
end
|
|
|
|
payload = { shell: options[:shell] || 'bash' }
|
|
payload[:network] = options[:network] if options[:network]
|
|
payload[:vcpu] = options[:vcpu] if options[:vcpu]
|
|
payload[:persistence] = 'tmux' if options[:tmux]
|
|
payload[:persistence] = 'screen' if options[:screen]
|
|
payload[:audit] = true if options[:audit]
|
|
|
|
puts "#{YELLOW}Creating session...#{RESET}"
|
|
result = api_request('/sessions', method: 'POST', data: payload, keys: keys)
|
|
puts "#{GREEN}Session created: #{result['id'] || 'N/A'}#{RESET}"
|
|
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
|
end
|
|
|
|
def validate_key(keys)
|
|
require 'openssl'
|
|
|
|
uri = URI("#{PORTAL_BASE}/keys/validate")
|
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
http.use_ssl = true
|
|
http.read_timeout = 30
|
|
|
|
timestamp = Time.now.to_i.to_s
|
|
body = ''
|
|
message = "#{timestamp}:POST:#{uri.path}:#{body}"
|
|
signature = OpenSSL::HMAC.hexdigest('SHA256', keys[:secret_key], message)
|
|
|
|
request = Net::HTTP::Post.new(uri)
|
|
request['Authorization'] = "Bearer #{keys[:public_key]}"
|
|
request['X-Timestamp'] = timestamp
|
|
request['X-Signature'] = signature
|
|
request['Content-Type'] = 'application/json'
|
|
|
|
response = http.request(request)
|
|
|
|
begin
|
|
result = JSON.parse(response.body)
|
|
rescue JSON::ParserError => e
|
|
warn "#{RED}Error: Failed to parse response: #{e.message}#{RESET}"
|
|
exit 1
|
|
end
|
|
|
|
if response.is_a?(Net::HTTPSuccess) && result['valid']
|
|
puts "#{GREEN}Valid#{RESET}"
|
|
puts "Public Key: #{result['public_key']}"
|
|
puts "Tier: #{result['tier']}"
|
|
puts "Status: #{result['status']}"
|
|
puts "Expires: #{result['expires_at']}"
|
|
puts "Time Remaining: #{result['time_remaining']}"
|
|
puts "Rate Limit: #{result['rate_limit']}"
|
|
puts "Burst: #{result['burst']}"
|
|
puts "Concurrency: #{result['concurrency']}"
|
|
result
|
|
elsif result['expired']
|
|
puts "#{RED}Expired#{RESET}"
|
|
puts "Public Key: #{result['public_key']}"
|
|
puts "Tier: #{result['tier']}"
|
|
puts "Expired: #{result['expires_at']}"
|
|
puts "#{YELLOW}To renew: Visit https://unsandbox.com/keys/extend#{RESET}"
|
|
result
|
|
else
|
|
puts "#{RED}Invalid#{RESET}"
|
|
puts "Error: #{result['error'] || result['reason'] || 'Unknown error'}"
|
|
exit 1
|
|
end
|
|
rescue => e
|
|
warn "#{RED}Error: #{e.message}#{RESET}"
|
|
exit 1
|
|
end
|
|
|
|
def open_browser(url)
|
|
case RbConfig::CONFIG['host_os']
|
|
when /mswin|mingw|cygwin/
|
|
system("start #{url}")
|
|
when /darwin/
|
|
system("open #{url}")
|
|
when /linux|bsd/
|
|
system("xdg-open #{url}")
|
|
else
|
|
puts "#{YELLOW}Please open this URL in your browser:#{RESET}"
|
|
puts url
|
|
end
|
|
end
|
|
|
|
def cmd_key(options)
|
|
keys = get_api_keys(options[:api_key])
|
|
|
|
if options[:extend]
|
|
result = validate_key(keys)
|
|
public_key = result['public_key']
|
|
if public_key
|
|
url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
|
|
puts "#{GREEN}Opening browser to extend key...#{RESET}"
|
|
open_browser(url)
|
|
else
|
|
warn "#{RED}Error: Could not retrieve public key#{RESET}"
|
|
exit 1
|
|
end
|
|
else
|
|
validate_key(keys)
|
|
end
|
|
end
|
|
|
|
def cmd_service(options)
|
|
keys = get_api_keys(options[:api_key])
|
|
|
|
if options[:list]
|
|
result = api_request('/services', keys: keys)
|
|
services = result['services'] || []
|
|
if services.empty?
|
|
puts 'No services'
|
|
else
|
|
puts format('%-20s %-15s %-10s %-15s %s', 'ID', 'Name', 'Status', 'Ports', 'Domains')
|
|
services.each do |s|
|
|
ports = (s['ports'] || []).join(',')
|
|
domains = (s['domains'] || []).join(',')
|
|
puts format('%-20s %-15s %-10s %-15s %s',
|
|
s['id'] || 'N/A', s['name'] || 'N/A',
|
|
s['status'] || 'N/A', ports, domains)
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if options[:info]
|
|
result = api_request("/services/#{options[:info]}", keys: keys)
|
|
puts JSON.pretty_generate(result)
|
|
return
|
|
end
|
|
|
|
if options[:logs]
|
|
result = api_request("/services/#{options[:logs]}/logs", keys: keys)
|
|
puts result['logs'] || ''
|
|
return
|
|
end
|
|
|
|
if options[:tail]
|
|
result = api_request("/services/#{options[:tail]}/logs?lines=9000", keys: keys)
|
|
puts result['logs'] || ''
|
|
return
|
|
end
|
|
|
|
if options[:sleep]
|
|
api_request("/services/#{options[:sleep]}/sleep", method: 'POST', keys: keys)
|
|
puts "#{GREEN}Service sleeping: #{options[:sleep]}#{RESET}"
|
|
return
|
|
end
|
|
|
|
if options[:wake]
|
|
api_request("/services/#{options[:wake]}/wake", method: 'POST', keys: keys)
|
|
puts "#{GREEN}Service waking: #{options[:wake]}#{RESET}"
|
|
return
|
|
end
|
|
|
|
if options[:destroy]
|
|
api_request("/services/#{options[:destroy]}", method: 'DELETE', keys: keys)
|
|
puts "#{GREEN}Service destroyed: #{options[:destroy]}#{RESET}"
|
|
return
|
|
end
|
|
|
|
if options[:execute]
|
|
payload = { command: options[:command] }
|
|
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, keys: keys)
|
|
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
|
|
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
|
|
return
|
|
end
|
|
|
|
if options[:dump_bootstrap]
|
|
warn "Fetching bootstrap script from #{options[:dump_bootstrap]}..."
|
|
payload = { command: 'cat /tmp/bootstrap.sh' }
|
|
result = api_request("/services/#{options[:dump_bootstrap]}/execute", method: 'POST', data: payload, keys: keys)
|
|
|
|
if result['stdout']
|
|
bootstrap = result['stdout']
|
|
if options[:dump_file]
|
|
# Write to file
|
|
begin
|
|
File.write(options[:dump_file], bootstrap)
|
|
File.chmod(0755, options[:dump_file])
|
|
puts "Bootstrap saved to #{options[:dump_file]}"
|
|
rescue => e
|
|
warn "#{RED}Error: Could not write to #{options[:dump_file]}: #{e.message}#{RESET}"
|
|
exit 1
|
|
end
|
|
else
|
|
# Print to stdout
|
|
print bootstrap
|
|
end
|
|
else
|
|
warn "#{RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{RESET}"
|
|
exit 1
|
|
end
|
|
return
|
|
end
|
|
|
|
if options[:name]
|
|
payload = { name: options[:name] }
|
|
payload[:ports] = options[:ports].split(',').map(&:to_i) if options[:ports]
|
|
payload[:domains] = options[:domains].split(',') if options[:domains]
|
|
payload[:service_type] = options[:type] if options[:type]
|
|
if options[:bootstrap]
|
|
payload[:bootstrap] = if File.exist?(options[:bootstrap])
|
|
File.read(options[:bootstrap])
|
|
else
|
|
options[:bootstrap]
|
|
end
|
|
end
|
|
payload[:network] = options[:network] if options[:network]
|
|
payload[:vcpu] = options[:vcpu] if options[:vcpu]
|
|
|
|
result = api_request('/services', method: 'POST', data: payload, keys: keys)
|
|
puts "#{GREEN}Service created: #{result['id'] || 'N/A'}#{RESET}"
|
|
puts "Name: #{result['name'] || 'N/A'}"
|
|
puts "URL: #{result['url']}" if result['url']
|
|
return
|
|
end
|
|
|
|
warn "#{RED}Error: Specify --name to create a service, or use --list, --info, etc.#{RESET}"
|
|
exit 1
|
|
end
|
|
|
|
def main
|
|
options = {
|
|
command: nil,
|
|
source_file: nil,
|
|
env: [],
|
|
files: [],
|
|
artifacts: false,
|
|
output_dir: nil,
|
|
network: nil,
|
|
vcpu: nil,
|
|
api_key: nil,
|
|
shell: nil,
|
|
list: false,
|
|
attach: nil,
|
|
kill: nil,
|
|
audit: false,
|
|
tmux: false,
|
|
screen: false,
|
|
name: nil,
|
|
ports: nil,
|
|
domains: nil,
|
|
type: nil,
|
|
bootstrap: nil,
|
|
info: nil,
|
|
logs: nil,
|
|
tail: nil,
|
|
sleep: nil,
|
|
wake: nil,
|
|
destroy: nil,
|
|
execute: nil,
|
|
dump_bootstrap: nil,
|
|
dump_file: nil,
|
|
extend: false
|
|
}
|
|
|
|
# Manual argument parsing
|
|
i = 0
|
|
while i < ARGV.length
|
|
arg = ARGV[i]
|
|
|
|
case arg
|
|
when 'session', 'service', 'key'
|
|
options[:command] = arg
|
|
when '-e'
|
|
i += 1
|
|
options[:env] << ARGV[i]
|
|
when '-f'
|
|
i += 1
|
|
options[:files] << ARGV[i]
|
|
when '-a'
|
|
options[:artifacts] = true
|
|
when '-o'
|
|
i += 1
|
|
options[:output_dir] = ARGV[i]
|
|
when '-n'
|
|
i += 1
|
|
options[:network] = ARGV[i]
|
|
when '-v'
|
|
i += 1
|
|
options[:vcpu] = ARGV[i].to_i
|
|
when '-k'
|
|
i += 1
|
|
options[:api_key] = ARGV[i]
|
|
when '-s', '--shell'
|
|
i += 1
|
|
options[:shell] = ARGV[i]
|
|
when '-l', '--list'
|
|
options[:list] = true
|
|
when '--attach'
|
|
i += 1
|
|
options[:attach] = ARGV[i]
|
|
when '--kill'
|
|
i += 1
|
|
options[:kill] = ARGV[i]
|
|
when '--audit'
|
|
options[:audit] = true
|
|
when '--tmux'
|
|
options[:tmux] = true
|
|
when '--screen'
|
|
options[:screen] = true
|
|
when '--name'
|
|
i += 1
|
|
options[:name] = ARGV[i]
|
|
when '--ports'
|
|
i += 1
|
|
options[:ports] = ARGV[i]
|
|
when '--domains'
|
|
i += 1
|
|
options[:domains] = ARGV[i]
|
|
when '--type'
|
|
i += 1
|
|
options[:type] = ARGV[i]
|
|
when '--bootstrap'
|
|
i += 1
|
|
options[:bootstrap] = ARGV[i]
|
|
when '--info'
|
|
i += 1
|
|
options[:info] = ARGV[i]
|
|
when '--logs'
|
|
i += 1
|
|
options[:logs] = ARGV[i]
|
|
when '--tail'
|
|
i += 1
|
|
options[:tail] = ARGV[i]
|
|
when '--freeze'
|
|
i += 1
|
|
options[:sleep] = ARGV[i]
|
|
when '--unfreeze'
|
|
i += 1
|
|
options[:wake] = ARGV[i]
|
|
when '--destroy'
|
|
i += 1
|
|
options[:destroy] = ARGV[i]
|
|
when '--execute'
|
|
i += 1
|
|
options[:execute] = ARGV[i]
|
|
when '--command'
|
|
i += 1
|
|
options[:command] = ARGV[i]
|
|
when '--dump-bootstrap'
|
|
i += 1
|
|
options[:dump_bootstrap] = ARGV[i]
|
|
when '--dump-file'
|
|
i += 1
|
|
options[:dump_file] = ARGV[i]
|
|
when '--extend'
|
|
options[:extend] = true
|
|
else
|
|
options[:source_file] = arg unless arg.start_with?('-')
|
|
end
|
|
|
|
i += 1
|
|
end
|
|
|
|
case options[:command]
|
|
when 'session'
|
|
cmd_session(options)
|
|
when 'service'
|
|
cmd_service(options)
|
|
when 'key'
|
|
cmd_key(options)
|
|
else
|
|
if options[:source_file]
|
|
cmd_execute(options)
|
|
else
|
|
puts <<~HELP
|
|
Unsandbox CLI - Execute code in secure sandboxes
|
|
|
|
Usage:
|
|
#{$PROGRAM_NAME} [options] <source_file>
|
|
#{$PROGRAM_NAME} session [options]
|
|
#{$PROGRAM_NAME} service [options]
|
|
#{$PROGRAM_NAME} key [options]
|
|
|
|
Execute options:
|
|
-e KEY=VALUE Environment variable (multiple allowed)
|
|
-f FILE Input file (multiple allowed)
|
|
-a Return artifacts
|
|
-o DIR Output directory for artifacts
|
|
-n MODE Network mode (zerotrust|semitrusted)
|
|
-v N vCPU count (1-8)
|
|
-k KEY API key
|
|
|
|
Session options:
|
|
-s, --shell NAME Shell/REPL (default: bash)
|
|
-l, --list List sessions
|
|
--attach ID Attach to session
|
|
--kill ID Terminate session
|
|
--audit Record session
|
|
--tmux Enable tmux persistence
|
|
--screen Enable screen persistence
|
|
|
|
Service options:
|
|
--name NAME Service name
|
|
--ports PORTS Comma-separated ports
|
|
--domains DOMAINS Custom domains
|
|
--type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp)
|
|
--bootstrap CMD Bootstrap command/file
|
|
-l, --list List services
|
|
--info ID Get service details
|
|
--logs ID Get all logs
|
|
--tail ID Get last 9000 lines
|
|
--freeze ID Freeze service
|
|
--unfreeze ID Unfreeze service
|
|
--destroy ID Destroy service
|
|
--execute ID Execute command in service
|
|
--command CMD Command to execute (with --execute)
|
|
--dump-bootstrap ID Dump bootstrap script
|
|
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
|
|
|
Key options:
|
|
-k KEY API key (or use UNSANDBOX_API_KEY env var)
|
|
--extend Validate key and open browser to extend
|
|
HELP
|
|
exit 1
|
|
end
|
|
end
|
|
end
|
|
|
|
main if __FILE__ == $PROGRAM_NAME
|