CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — Dart
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/dart/sync/src/un.dart && chmod +x un.dart && ln -sf un.dart un
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.dart
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app Dart existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/dart/sync/src/un.dart
# Option A: Environment variables
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Option B: Config file (persistent)
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
// In your Dart app:
import 'un.dart';
void main() async {
var result = await executeCode("dart", "print('Hello from Dart running on unsandbox!');");
print(result['stdout']); // Hello from Dart running on unsandbox!
}
dart run myapp.dart
121c80c904edd0f2d1f0a916c70fac9b
SHA256: 71650c47a76ddc6c52e26161b17f42e30491b591a86cea8ed4c3cc334ca59366
// 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
// un.dart - Unsandbox CLI Client (Dart Implementation)
// Run: dart un.dart [options] <source_file>
// Compile: dart compile exe un.dart -o un
// Requires: UNSANDBOX_API_KEY environment variable
import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';
const String apiBase = 'https://api.unsandbox.com';
const String portalBase = 'https://unsandbox.com';
const String blue = '\x1B[34m';
const String red = '\x1B[31m';
const String green = '\x1B[32m';
const String yellow = '\x1B[33m';
const String reset = '\x1B[0m';
const int languagesCacheTtl = 3600; // 1 hour in seconds
const Map<String, String> extMap = {
'.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',
};
class Args {
String? command;
String? sourceFile;
String? apiKey;
String? publicKey;
int? account;
String? network;
int vcpu = 0;
List<String> env = [];
List<String> files = [];
bool artifacts = false;
String? outputDir;
bool sessionList = false;
String? sessionShell;
String? sessionKill;
bool serviceList = false;
bool languagesJson = false;
String? serviceName;
String? servicePorts;
String? serviceType;
String? serviceBootstrap;
String? serviceBootstrapFile;
String? serviceInfo;
String? serviceLogs;
String? serviceTail;
String? serviceSleep;
String? serviceWake;
String? serviceDestroy;
String? serviceResize;
int serviceResizeVcpu = 0;
String? serviceExecute;
String? serviceCommand;
String? serviceDumpBootstrap;
String? serviceDumpFile;
String? serviceUnfreezeOnDemand;
bool serviceUnfreezeOnDemandEnabled = true;
bool serviceCreateUnfreezeOnDemand = false;
String? serviceShowFreezePage;
bool serviceShowFreezePageEnabled = true;
bool keyExtend = false;
String? envFile;
String? envAction;
String? envTarget;
// Image command options
bool imageList = false;
String? imageInfo;
String? imageDelete;
String? imageLock;
String? imageUnlock;
String? imagePublish;
String? imageSourceType;
String? imageVisibility;
String? imageVisibilityMode;
String? imageSpawn;
String? imageClone;
String? imageName;
String? imagePorts;
// Snapshot command options
bool snapshotList = false;
String? snapshotInfo;
String? snapshotSession;
String? snapshotService;
String? snapshotRestore;
String? snapshotDelete;
String? snapshotLock;
String? snapshotUnlock;
String? snapshotClone;
String? snapshotCloneType;
String? snapshotName;
String? snapshotPorts;
String? snapshotShell;
bool snapshotHot = false;
}
Map<String, String>? loadAccountsCSV(String path, int index) {
try {
final file = File(path);
if (!file.existsSync()) return null;
final lines = file.readAsLinesSync().where((l) {
final t = l.trim();
return t.isNotEmpty && !t.startsWith('#');
}).toList();
if (index < 0 || index >= lines.length) return null;
final parts = lines[index].split(',');
if (parts.length < 2) return null;
final pk = parts[0].trim();
final sk = parts[1].trim();
if (pk.isEmpty || sk.isEmpty) return null;
return {'pk': pk, 'sk': sk};
} catch (e) {
return null;
}
}
List<String?> getApiKeys(String? argsKey, {String? argsPublicKey, int? account}) {
// Tier 1: explicit -p/-k flags
if (argsPublicKey != null && argsPublicKey.isNotEmpty && argsKey != null && argsKey.isNotEmpty) {
return [argsPublicKey, argsKey];
}
// Tier 2: --account N → accounts.csv row N (bypasses env vars)
if (account != null) {
final home = Platform.environment['HOME'] ?? '';
if (home.isNotEmpty) {
final fromHome = loadAccountsCSV('$home/.unsandbox/accounts.csv', account);
if (fromHome != null) return [fromHome['pk'], fromHome['sk']];
}
final fromLocal = loadAccountsCSV('./accounts.csv', account);
if (fromLocal != null) return [fromLocal['pk'], fromLocal['sk']];
stderr.writeln('${red}Error: --account $account not found in accounts.csv$reset');
exit(1);
}
// Tier 3: env vars
final envPk = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
final envSk = Platform.environment['UNSANDBOX_SECRET_KEY'];
if (envPk != null && envPk.isNotEmpty && envSk != null && envSk.isNotEmpty) {
return [envPk, envSk];
}
// Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
final home = Platform.environment['HOME'] ?? '';
final defIndexStr = Platform.environment['UNSANDBOX_ACCOUNT'] ?? '0';
final defIndex = int.tryParse(defIndexStr) ?? 0;
if (home.isNotEmpty) {
final fromHome = loadAccountsCSV('$home/.unsandbox/accounts.csv', defIndex);
if (fromHome != null) return [fromHome['pk'], fromHome['sk']];
}
// Tier 5: ./accounts.csv row 0
final fromLocal = loadAccountsCSV('./accounts.csv', defIndex);
if (fromLocal != null) return [fromLocal['pk'], fromLocal['sk']];
// Legacy UNSANDBOX_API_KEY fallback
final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
if (legacyKey != null && legacyKey.isNotEmpty) {
return [legacyKey, null];
}
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
exit(1);
}
String detectLanguage(String filename) {
final dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1) {
throw Exception('Cannot detect language: no file extension');
}
final ext = filename.substring(dotIndex).toLowerCase();
final lang = extMap[ext];
if (lang == null) {
throw Exception('Unsupported file extension: $ext');
}
return lang;
}
String? getLanguagesCachePath() {
final home = Platform.environment['HOME'];
if (home == null || home.isEmpty) return null;
return '$home/.unsandbox/languages.json';
}
Future<Map<String, dynamic>?> loadLanguagesCache() async {
final cachePath = getLanguagesCachePath();
if (cachePath == null) return null;
final cacheFile = File(cachePath);
if (!await cacheFile.exists()) return null;
try {
final content = await cacheFile.readAsString();
final data = jsonDecode(content) as Map<String, dynamic>;
final cachedTime = data['timestamp'] as int?;
if (cachedTime == null) return null;
final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Check if cache is still valid (within TTL)
if (currentTime - cachedTime < languagesCacheTtl) {
return data;
}
} catch (e) {
// Cache read failed, return null to fetch fresh
}
return null;
}
Future<void> saveLanguagesCache(Map<String, dynamic> response) async {
final cachePath = getLanguagesCachePath();
if (cachePath == null) return;
try {
final cacheFile = File(cachePath);
// Ensure directory exists
final cacheDir = cacheFile.parent;
if (!await cacheDir.exists()) {
await cacheDir.create(recursive: true);
}
// Extract languages from response
final languages = response['languages'];
if (languages == null) return;
// Build cache JSON with timestamp
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final cacheData = {
'languages': languages,
'timestamp': timestamp,
};
await cacheFile.writeAsString(jsonEncode(cacheData));
} catch (e) {
// Cache write failed, ignore
}
}
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String publicKey, String? secretKey, {String? baseUrl}) async {
final base = baseUrl ?? apiBase;
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
try {
final body = jsonData ?? '';
if (jsonData != null) {
await tempFile.writeAsString(jsonData);
}
final args = ['curl', '-s', '-X', method, '$base$endpoint',
'-H', 'Content-Type: application/json'];
// Add HMAC authentication headers if secretKey is provided
if (secretKey != null && secretKey.isNotEmpty) {
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
final message = '$timestamp:$method:$endpoint:$body';
final key = utf8.encode(secretKey);
final bytes = utf8.encode(message);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final signature = digest.toString();
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
args.addAll(['-H', 'X-Timestamp: $timestamp']);
args.addAll(['-H', 'X-Signature: $signature']);
} else {
// Legacy API key authentication
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
}
if (jsonData != null) {
args.addAll(['-d', '@${tempFile.path}']);
}
final result = await Process.run(args[0], args.sublist(1));
if (result.exitCode != 0) {
throw Exception('curl failed: ${result.stderr}');
}
final response = result.stdout as String;
// Check for timestamp authentication errors
if (response.toLowerCase().contains('timestamp') &&
(response.contains('401') || response.toLowerCase().contains('expired') || response.toLowerCase().contains('invalid'))) {
stderr.writeln('${red}Error: Request timestamp expired (must be within 5 minutes of server time)$reset');
stderr.writeln('${yellow}Your computer\'s clock may have drifted.$reset');
stderr.writeln('Check your system time and sync with NTP if needed:');
stderr.writeln(' Linux: sudo ntpdate -s time.nist.gov');
stderr.writeln(' macOS: sudo sntp -sS time.apple.com');
stderr.writeln(' Windows: w32tm /resync');
exit(1);
}
return jsonDecode(response) as Map<String, dynamic>;
} finally {
await tempFile.delete();
}
}
/// Make authenticated API request returning status code and body
Future<(int, String)> apiRequestCurlWithStatus(String endpoint, String method, String? jsonData, String publicKey, String? secretKey, {String? baseUrl, String? sudoOtp, String? sudoChallenge}) async {
final base = baseUrl ?? apiBase;
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
try {
final body = jsonData ?? '';
if (jsonData != null) {
await tempFile.writeAsString(jsonData);
}
final args = ['curl', '-s', '-w', '%{http_code}', '-X', method, '$base$endpoint',
'-H', 'Content-Type: application/json'];
// Add HMAC authentication headers if secretKey is provided
if (secretKey != null && secretKey.isNotEmpty) {
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
final message = '$timestamp:$method:$endpoint:$body';
final key = utf8.encode(secretKey);
final bytes = utf8.encode(message);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final signature = digest.toString();
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
args.addAll(['-H', 'X-Timestamp: $timestamp']);
args.addAll(['-H', 'X-Signature: $signature']);
} else {
// Legacy API key authentication
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
}
// Add sudo OTP headers if provided
if (sudoOtp != null && sudoChallenge != null) {
args.addAll(['-H', 'X-Sudo-OTP: $sudoOtp']);
args.addAll(['-H', 'X-Sudo-Challenge: $sudoChallenge']);
}
if (jsonData != null) {
args.addAll(['-d', '@${tempFile.path}']);
}
final result = await Process.run(args[0], args.sublist(1));
if (result.exitCode != 0) {
throw Exception('curl failed: ${result.stderr}');
}
final output = result.stdout as String;
// Extract status code from end of output (last 3 chars)
int statusCode = 0;
String responseBody = output;
if (output.length >= 3) {
final codeStr = output.substring(output.length - 3);
statusCode = int.tryParse(codeStr) ?? 0;
responseBody = output.substring(0, output.length - 3);
}
return (statusCode, responseBody);
} finally {
await tempFile.delete();
}
}
/// Handle HTTP 428 sudo OTP challenge
/// Returns true if retry succeeded, false otherwise
Future<bool> handleSudoChallenge(String responseBody, String endpoint, String method, String? jsonData, String publicKey, String? secretKey) async {
// Extract challenge_id from response
String? challengeId;
try {
final resp = jsonDecode(responseBody) as Map<String, dynamic>;
challengeId = resp['challenge_id'] as String?;
} catch (e) {
stderr.writeln('${red}Error: Could not parse challenge response$reset');
return false;
}
if (challengeId == null || challengeId.isEmpty) {
stderr.writeln('${red}Error: Could not extract challenge_id from response$reset');
return false;
}
// Prompt user for OTP
stderr.writeln('${yellow}Confirmation required. Check your email for a one-time code.$reset');
stderr.write('Enter OTP: ');
final otp = stdin.readLineSync()?.trim();
if (otp == null || otp.isEmpty) {
stderr.writeln('${red}Error: No OTP provided$reset');
return false;
}
// Retry request with sudo headers
final (statusCode, _) = await apiRequestCurlWithStatus(
endpoint, method, jsonData, publicKey, secretKey,
sudoOtp: otp, sudoChallenge: challengeId
);
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
stderr.writeln('${red}Error: OTP verification failed$reset');
return false;
}
}
Future<Map<String, dynamic>?> apiRequestTextCurl(String endpoint, String method, String body, String publicKey, String? secretKey) async {
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.txt').create();
try {
await tempFile.writeAsString(body);
final args = ['curl', '-s', '-X', method, '$apiBase$endpoint',
'-H', 'Content-Type: text/plain'];
if (secretKey != null && secretKey.isNotEmpty) {
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
final message = '$timestamp:$method:$endpoint:$body';
final key = utf8.encode(secretKey);
final bytes = utf8.encode(message);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final signature = digest.toString();
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
args.addAll(['-H', 'X-Timestamp: $timestamp']);
args.addAll(['-H', 'X-Signature: $signature']);
} else {
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
}
args.addAll(['-d', '@${tempFile.path}', '-w', '%{http_code}']);
final result = await Process.run(args[0], args.sublist(1));
final output = result.stdout as String;
// Last 3 characters are the status code
if (output.length >= 3) {
final statusCode = int.tryParse(output.substring(output.length - 3)) ?? 0;
final responseBody = output.substring(0, output.length - 3);
if (statusCode >= 200 && statusCode < 300) {
if (responseBody.isNotEmpty) {
try {
return jsonDecode(responseBody) as Map<String, dynamic>;
} catch (e) {
return {'success': true};
}
}
return {'success': true};
}
}
return null;
} finally {
await tempFile.delete();
}
}
const int maxEnvContentSize = 65536;
Future<String> readEnvFile(String path) async {
final file = File(path);
if (!await file.exists()) {
stderr.writeln('${red}Error: Env file not found: $path$reset');
exit(1);
}
return await file.readAsString();
}
Future<String> buildEnvContent(List<String> envs, String? envFile) async {
final lines = <String>[];
lines.addAll(envs);
if (envFile != null) {
final content = await readEnvFile(envFile);
for (final line in content.split('\n')) {
final trimmed = line.trim();
if (trimmed.isNotEmpty && !trimmed.startsWith('#')) {
lines.add(trimmed);
}
}
}
return lines.join('\n');
}
Future<Map<String, dynamic>> serviceEnvStatus(String serviceId, String publicKey, String? secretKey) async {
return await apiRequestCurl('/services/$serviceId/env', 'GET', null, publicKey, secretKey);
}
Future<bool> serviceEnvSet(String serviceId, String envContent, String publicKey, String? secretKey) async {
if (envContent.length > maxEnvContentSize) {
stderr.writeln('${red}Error: Env content exceeds maximum size of 64KB$reset');
return false;
}
final result = await apiRequestTextCurl('/services/$serviceId/env', 'PUT', envContent, publicKey, secretKey);
return result != null;
}
Future<Map<String, dynamic>> serviceEnvExport(String serviceId, String publicKey, String? secretKey) async {
return await apiRequestCurl('/services/$serviceId/env/export', 'POST', '{}', publicKey, secretKey);
}
Future<bool> serviceEnvDelete(String serviceId, String publicKey, String? secretKey) async {
try {
await apiRequestCurl('/services/$serviceId/env', 'DELETE', null, publicKey, secretKey);
return true;
} catch (e) {
return false;
}
}
Future<void> cmdServiceEnv(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
final action = args.envAction;
final target = args.envTarget;
switch (action) {
case 'status':
if (target == null) {
stderr.writeln('${red}Error: service env status requires service ID$reset');
exit(1);
}
final result = await serviceEnvStatus(target, publicKey, secretKey);
final hasVault = result['has_vault'] as bool? ?? false;
if (hasVault) {
print('${green}Vault: configured$reset');
final envCount = result['env_count'];
if (envCount != null) print('Variables: $envCount');
final updatedAt = result['updated_at'];
if (updatedAt != null) print('Updated: $updatedAt');
} else {
print('${yellow}Vault: not configured$reset');
}
break;
case 'set':
if (target == null) {
stderr.writeln('${red}Error: service env set requires service ID$reset');
exit(1);
}
if (args.env.isEmpty && args.envFile == null) {
stderr.writeln('${red}Error: service env set requires -e or --env-file$reset');
exit(1);
}
final envContent = await buildEnvContent(args.env, args.envFile);
if (await serviceEnvSet(target, envContent, publicKey, secretKey)) {
print('${green}Vault updated for service $target$reset');
} else {
stderr.writeln('${red}Error: Failed to update vault$reset');
exit(1);
}
break;
case 'export':
if (target == null) {
stderr.writeln('${red}Error: service env export requires service ID$reset');
exit(1);
}
final result = await serviceEnvExport(target, publicKey, secretKey);
final content = result['content'] as String?;
if (content != null) stdout.write(content);
break;
case 'delete':
if (target == null) {
stderr.writeln('${red}Error: service env delete requires service ID$reset');
exit(1);
}
if (await serviceEnvDelete(target, publicKey, secretKey)) {
print('${green}Vault deleted for service $target$reset');
} else {
stderr.writeln('${red}Error: Failed to delete vault$reset');
exit(1);
}
break;
default:
stderr.writeln('${red}Error: Unknown env action: $action$reset');
stderr.writeln('Usage: dart un.dart service env <status|set|export|delete> <service_id>');
exit(1);
}
}
Future<void> cmdExecute(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
final code = await File(args.sourceFile!).readAsString();
final language = detectLanguage(args.sourceFile!);
final payload = <String, dynamic>{
'language': language,
'code': code,
};
if (args.env.isNotEmpty) {
final envVars = <String, String>{};
for (final e in args.env) {
final parts = e.split('=');
if (parts.length == 2) {
envVars[parts[0]] = parts[1];
}
}
if (envVars.isNotEmpty) {
payload['env'] = envVars;
}
}
if (args.files.isNotEmpty) {
final inputFiles = <Map<String, String>>[];
for (final filepath in args.files) {
final content = await File(filepath).readAsBytes();
inputFiles.add({
'filename': filepath.split('/').last,
'content_base64': base64Encode(content),
});
}
payload['input_files'] = inputFiles;
}
if (args.artifacts) {
payload['return_artifacts'] = true;
}
if (args.network != null) {
payload['network'] = args.network;
}
if (args.vcpu > 0) {
payload['vcpu'] = args.vcpu;
}
final result = await apiRequestCurl('/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final stdoutText = result['stdout'] as String?;
final stderrText = result['stderr'] as String?;
if (stdoutText != null && stdoutText.isNotEmpty) {
stdout.write('$blue$stdoutText$reset');
}
if (stderrText != null && stderrText.isNotEmpty) {
stderr.write('$red$stderrText$reset');
}
if (args.artifacts && result.containsKey('artifacts')) {
final artifacts = result['artifacts'] as List;
final outDir = args.outputDir ?? '.';
await Directory(outDir).create(recursive: true);
for (final artifact in artifacts) {
final artifactMap = artifact as Map<String, dynamic>;
final filename = artifactMap['filename'] as String? ?? 'artifact';
final content = base64Decode(artifactMap['content_base64'] as String);
final file = File('$outDir/$filename');
await file.writeAsBytes(content);
await Process.run('chmod', ['+x', file.path]);
stderr.writeln('${green}Saved: ${file.path}$reset');
}
}
final exitCode = result['exit_code'] as int? ?? 0;
exit(exitCode);
}
Future<void> cmdSession(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
if (args.sessionList) {
final result = await apiRequestCurl('/sessions', 'GET', null, publicKey, secretKey);
final sessions = result['sessions'] as List? ?? [];
if (sessions.isEmpty) {
print('No active sessions');
} else {
print('${'ID'.padRight(40)} ${'Shell'.padRight(10)} ${'Status'.padRight(10)} Created');
for (final s in sessions) {
final session = s as Map<String, dynamic>;
print('${(session['id'] ?? 'N/A').toString().padRight(40)} ${(session['shell'] ?? 'N/A').toString().padRight(10)} ${(session['status'] ?? 'N/A').toString().padRight(10)} ${session['created_at'] ?? 'N/A'}');
}
}
return;
}
if (args.sessionKill != null) {
await apiRequestCurl('/sessions/${args.sessionKill}', 'DELETE', null, publicKey, secretKey);
print('${green}Session terminated: ${args.sessionKill}$reset');
return;
}
final payload = <String, dynamic>{
'shell': args.sessionShell ?? 'bash',
};
if (args.network != null) {
payload['network'] = args.network;
}
if (args.vcpu > 0) {
payload['vcpu'] = args.vcpu;
}
// Add input files
if (args.files.isNotEmpty) {
final inputFiles = <Map<String, String>>[];
for (final filepath in args.files) {
final file = File(filepath);
if (!await file.exists()) {
stderr.writeln('${red}Error: Input file not found: $filepath$reset');
exit(1);
}
final content = await file.readAsBytes();
inputFiles.add({
'filename': filepath.split('/').last,
'content_base64': base64Encode(content),
});
}
payload['input_files'] = inputFiles;
}
print('${yellow}Creating session...$reset');
final result = await apiRequestCurl('/sessions', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Session created: ${result['id'] ?? 'N/A'}$reset');
print('${yellow}(Interactive sessions require WebSocket - use un2 for full support)$reset');
}
Future<void> cmdService(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
// Handle env subcommand
if (args.envAction != null) {
await cmdServiceEnv(args);
return;
}
if (args.serviceList) {
final result = await apiRequestCurl('/services', 'GET', null, publicKey, secretKey);
final services = result['services'] as List? ?? [];
if (services.isEmpty) {
print('No services');
} else {
print('${'ID'.padRight(20)} ${'Name'.padRight(15)} ${'Status'.padRight(10)} ${'Ports'.padRight(15)} Domains');
for (final s in services) {
final service = s as Map<String, dynamic>;
final ports = (service['ports'] as List?)?.join(',') ?? '';
final domains = (service['domains'] as List?)?.join(',') ?? '';
print('${(service['id'] ?? 'N/A').toString().padRight(20)} ${(service['name'] ?? 'N/A').toString().padRight(15)} ${(service['status'] ?? 'N/A').toString().padRight(10)} ${ports.padRight(15)} $domains');
}
}
return;
}
if (args.serviceInfo != null) {
final result = await apiRequestCurl('/services/${args.serviceInfo}', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
return;
}
if (args.serviceLogs != null) {
final result = await apiRequestCurl('/services/${args.serviceLogs}/logs', 'GET', null, publicKey, secretKey);
print(result['logs'] ?? '');
return;
}
if (args.serviceTail != null) {
final result = await apiRequestCurl('/services/${args.serviceTail}/logs?lines=9000', 'GET', null, publicKey, secretKey);
print(result['logs'] ?? '');
return;
}
if (args.serviceSleep != null) {
await apiRequestCurl('/services/${args.serviceSleep}/freeze', 'POST', null, publicKey, secretKey);
print('${green}Service frozen: ${args.serviceSleep}$reset');
return;
}
if (args.serviceWake != null) {
await apiRequestCurl('/services/${args.serviceWake}/unfreeze', 'POST', null, publicKey, secretKey);
print('${green}Service unfreezing: ${args.serviceWake}$reset');
return;
}
if (args.serviceUnfreezeOnDemand != null) {
final payload = {'unfreeze_on_demand': args.serviceUnfreezeOnDemandEnabled};
await apiRequestCurl('/services/${args.serviceUnfreezeOnDemand}', 'PATCH', jsonEncode(payload), publicKey, secretKey);
final status = args.serviceUnfreezeOnDemandEnabled ? 'enabled' : 'disabled';
print('${green}Unfreeze-on-demand $status for service: ${args.serviceUnfreezeOnDemand}$reset');
return;
}
if (args.serviceShowFreezePage != null) {
final payload = {'show_freeze_page': args.serviceShowFreezePageEnabled};
await apiRequestCurl('/services/${args.serviceShowFreezePage}', 'PATCH', jsonEncode(payload), publicKey, secretKey);
final status = args.serviceShowFreezePageEnabled ? 'enabled' : 'disabled';
print('${green}Show-freeze-page $status for service: ${args.serviceShowFreezePage}$reset');
return;
}
if (args.serviceDestroy != null) {
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/services/${args.serviceDestroy}', 'DELETE', null, publicKey, secretKey);
if (statusCode == 428) {
if (await handleSudoChallenge(responseBody, '/services/${args.serviceDestroy}', 'DELETE', null, publicKey, secretKey)) {
print('${green}Service destroyed: ${args.serviceDestroy}$reset');
} else {
stderr.writeln('${red}Error: Failed to destroy service (OTP verification failed)$reset');
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
print('${green}Service destroyed: ${args.serviceDestroy}$reset');
} else {
stderr.writeln('${red}Error: Failed to destroy service (HTTP $statusCode)$reset');
exit(1);
}
return;
}
if (args.serviceResize != null) {
if (args.serviceResizeVcpu < 1 || args.serviceResizeVcpu > 8) {
stderr.writeln('${red}Error: --vcpu must be between 1 and 8$reset');
exit(1);
}
final payload = {'vcpu': args.serviceResizeVcpu};
await apiRequestCurl('/services/${args.serviceResize}', 'PATCH', jsonEncode(payload), publicKey, secretKey);
final ram = args.serviceResizeVcpu * 2;
print('${green}Service resized to ${args.serviceResizeVcpu} vCPU, $ram GB RAM$reset');
return;
}
if (args.serviceExecute != null) {
final payload = <String, dynamic>{
'command': args.serviceCommand,
};
final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final stdoutText = result['stdout'] as String?;
final stderrText = result['stderr'] as String?;
if (stdoutText != null && stdoutText.isNotEmpty) {
stdout.write('$blue$stdoutText$reset');
}
if (stderrText != null && stderrText.isNotEmpty) {
stderr.write('$red$stderrText$reset');
}
return;
}
if (args.serviceDumpBootstrap != null) {
stderr.writeln('Fetching bootstrap script from ${args.serviceDumpBootstrap}...');
final payload = <String, dynamic>{
'command': 'cat /tmp/bootstrap.sh',
};
final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final bootstrap = result['stdout'] as String?;
if (bootstrap != null && bootstrap.isNotEmpty) {
if (args.serviceDumpFile != null) {
try {
await File(args.serviceDumpFile!).writeAsString(bootstrap);
await Process.run('chmod', ['755', args.serviceDumpFile!]);
print('Bootstrap saved to ${args.serviceDumpFile}');
} catch (e) {
stderr.writeln('${red}Error: Could not write to ${args.serviceDumpFile}: $e$reset');
exit(1);
}
} else {
stdout.write(bootstrap);
}
} else {
stderr.writeln('${red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)$reset');
exit(1);
}
return;
}
if (args.serviceName != null) {
final payload = <String, dynamic>{
'name': args.serviceName!,
};
if (args.servicePorts != null) {
payload['ports'] = args.servicePorts!.split(',').map((p) => int.parse(p.trim())).toList();
}
if (args.serviceType != null) {
payload['service_type'] = args.serviceType;
}
if (args.serviceBootstrap != null) {
payload['bootstrap'] = args.serviceBootstrap;
}
if (args.serviceBootstrapFile != null) {
final file = File(args.serviceBootstrapFile!);
if (await file.exists()) {
payload['bootstrap_content'] = await file.readAsString();
} else {
stderr.writeln('${red}Error: Bootstrap file not found: ${args.serviceBootstrapFile}$reset');
exit(1);
}
}
if (args.network != null) {
payload['network'] = args.network;
}
if (args.vcpu > 0) {
payload['vcpu'] = args.vcpu;
}
if (args.serviceCreateUnfreezeOnDemand) {
payload['unfreeze_on_demand'] = true;
}
// Add input files
if (args.files.isNotEmpty) {
final inputFiles = <Map<String, String>>[];
for (final filepath in args.files) {
final file = File(filepath);
if (!await file.exists()) {
stderr.writeln('${red}Error: Input file not found: $filepath$reset');
exit(1);
}
final content = await file.readAsBytes();
inputFiles.add({
'filename': filepath.split('/').last,
'content_base64': base64Encode(content),
});
}
payload['input_files'] = inputFiles;
}
final result = await apiRequestCurl('/services', 'POST', jsonEncode(payload), publicKey, secretKey);
final serviceId = result['id'] as String?;
print('${green}Service created: ${serviceId ?? 'N/A'}$reset');
print('Name: ${result['name'] ?? 'N/A'}');
if (result.containsKey('url')) {
print('URL: ${result['url']}');
}
// Auto-set vault if env vars were provided
if (serviceId != null && (args.env.isNotEmpty || args.envFile != null)) {
final envContent = await buildEnvContent(args.env, args.envFile);
if (envContent.isNotEmpty) {
if (await serviceEnvSet(serviceId, envContent, publicKey, secretKey)) {
print('${green}Vault configured with environment variables$reset');
} else {
print('${yellow}Warning: Failed to set vault$reset');
}
}
}
return;
}
stderr.writeln('${red}Error: Specify --name to create a service, or use --list, --info, etc.$reset');
exit(1);
}
Future<void> cmdLanguages(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
// Try to load from cache first
var cachedData = await loadLanguagesCache();
Map<String, dynamic> result;
if (cachedData != null) {
result = cachedData;
} else {
// Fetch from API
result = await apiRequestCurl('/languages', 'GET', null, publicKey, secretKey);
// Save to cache
await saveLanguagesCache(result);
}
final languages = result['languages'] as List? ?? [];
if (args.languagesJson) {
// JSON output: print as array
print(jsonEncode(languages));
} else {
// Default: one language per line
for (final lang in languages) {
print(lang.toString());
}
}
}
Future<void> cmdImage(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
if (args.imageList) {
final result = await apiRequestCurl('/images', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
return;
}
if (args.imageInfo != null) {
final result = await apiRequestCurl('/images/${args.imageInfo}', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
return;
}
if (args.imageDelete != null) {
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/images/${args.imageDelete}', 'DELETE', null, publicKey, secretKey);
if (statusCode == 428) {
if (await handleSudoChallenge(responseBody, '/images/${args.imageDelete}', 'DELETE', null, publicKey, secretKey)) {
print('${green}Image deleted: ${args.imageDelete}$reset');
} else {
stderr.writeln('${red}Error: Failed to delete image (OTP verification failed)$reset');
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
print('${green}Image deleted: ${args.imageDelete}$reset');
} else {
stderr.writeln('${red}Error: Failed to delete image (HTTP $statusCode)$reset');
exit(1);
}
return;
}
if (args.imageLock != null) {
await apiRequestCurl('/images/${args.imageLock}/lock', 'POST', null, publicKey, secretKey);
print('${green}Image locked: ${args.imageLock}$reset');
return;
}
if (args.imageUnlock != null) {
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/images/${args.imageUnlock}/unlock', 'POST', null, publicKey, secretKey);
if (statusCode == 428) {
if (await handleSudoChallenge(responseBody, '/images/${args.imageUnlock}/unlock', 'POST', null, publicKey, secretKey)) {
print('${green}Image unlocked: ${args.imageUnlock}$reset');
} else {
stderr.writeln('${red}Error: Failed to unlock image (OTP verification failed)$reset');
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
print('${green}Image unlocked: ${args.imageUnlock}$reset');
} else {
stderr.writeln('${red}Error: Failed to unlock image (HTTP $statusCode)$reset');
exit(1);
}
return;
}
if (args.imagePublish != null) {
if (args.imageSourceType == null) {
stderr.writeln('${red}Error: --source-type required (service or snapshot)$reset');
exit(1);
}
final payload = <String, dynamic>{
'source_type': args.imageSourceType,
'source_id': args.imagePublish,
};
if (args.imageName != null) {
payload['name'] = args.imageName;
}
final result = await apiRequestCurl('/images/publish', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image published$reset');
print(jsonEncode(result));
return;
}
if (args.imageVisibility != null) {
if (args.imageVisibilityMode == null) {
stderr.writeln('${red}Error: --visibility requires MODE (private, unlisted, or public)$reset');
exit(1);
}
final payload = {'visibility': args.imageVisibilityMode};
await apiRequestCurl('/images/${args.imageVisibility}/visibility', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image visibility set to ${args.imageVisibilityMode}$reset');
return;
}
if (args.imageSpawn != null) {
final payload = <String, dynamic>{};
if (args.imageName != null) {
payload['name'] = args.imageName;
}
if (args.imagePorts != null) {
payload['ports'] = args.imagePorts!.split(',').map((p) => int.parse(p.trim())).toList();
}
final result = await apiRequestCurl('/images/${args.imageSpawn}/spawn', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Service spawned from image$reset');
print(jsonEncode(result));
return;
}
if (args.imageClone != null) {
final payload = <String, dynamic>{};
if (args.imageName != null) {
payload['name'] = args.imageName;
}
final result = await apiRequestCurl('/images/${args.imageClone}/clone', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image cloned$reset');
print(jsonEncode(result));
return;
}
stderr.writeln('${red}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID$reset');
exit(1);
}
// Image access management functions
Future<void> imageGrantAccess(String id, String trustedKey, String publicKey, String? secretKey) async {
final payload = {'trusted_api_key': trustedKey};
await apiRequestCurl('/images/$id/grant-access', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Access granted to: $trustedKey$reset');
}
Future<void> imageRevokeAccess(String id, String trustedKey, String publicKey, String? secretKey) async {
final payload = {'trusted_api_key': trustedKey};
await apiRequestCurl('/images/$id/revoke-access', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Access revoked from: $trustedKey$reset');
}
Future<void> imageListTrusted(String id, String publicKey, String? secretKey) async {
final result = await apiRequestCurl('/images/$id/trusted', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
}
Future<void> imageTransfer(String id, String toKey, String publicKey, String? secretKey) async {
final payload = {'to_api_key': toKey};
await apiRequestCurl('/images/$id/transfer', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image transferred to: $toKey$reset');
}
// Snapshot functions
Future<void> cmdSnapshot(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
if (args.snapshotList) {
final result = await apiRequestCurl('/snapshots', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
return;
}
if (args.snapshotInfo != null) {
final result = await apiRequestCurl('/snapshots/${args.snapshotInfo}', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
return;
}
if (args.snapshotSession != null) {
final payload = <String, dynamic>{
'session_id': args.snapshotSession,
};
if (args.snapshotName != null) {
payload['name'] = args.snapshotName;
}
if (args.snapshotHot) {
payload['hot'] = true;
}
final result = await apiRequestCurl('/snapshots', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Snapshot created$reset');
print(jsonEncode(result));
return;
}
if (args.snapshotService != null) {
final payload = <String, dynamic>{
'service_id': args.snapshotService,
};
if (args.snapshotName != null) {
payload['name'] = args.snapshotName;
}
if (args.snapshotHot) {
payload['hot'] = true;
}
final result = await apiRequestCurl('/snapshots', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Snapshot created$reset');
print(jsonEncode(result));
return;
}
if (args.snapshotRestore != null) {
await apiRequestCurl('/snapshots/${args.snapshotRestore}/restore', 'POST', '{}', publicKey, secretKey);
print('${green}Snapshot restored: ${args.snapshotRestore}$reset');
return;
}
if (args.snapshotDelete != null) {
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/snapshots/${args.snapshotDelete}', 'DELETE', null, publicKey, secretKey);
if (statusCode == 428) {
if (await handleSudoChallenge(responseBody, '/snapshots/${args.snapshotDelete}', 'DELETE', null, publicKey, secretKey)) {
print('${green}Snapshot deleted: ${args.snapshotDelete}$reset');
} else {
stderr.writeln('${red}Error: Failed to delete snapshot (OTP verification failed)$reset');
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
print('${green}Snapshot deleted: ${args.snapshotDelete}$reset');
} else {
stderr.writeln('${red}Error: Failed to delete snapshot (HTTP $statusCode)$reset');
exit(1);
}
return;
}
if (args.snapshotLock != null) {
await apiRequestCurl('/snapshots/${args.snapshotLock}/lock', 'POST', '{}', publicKey, secretKey);
print('${green}Snapshot locked: ${args.snapshotLock}$reset');
return;
}
if (args.snapshotUnlock != null) {
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/snapshots/${args.snapshotUnlock}/unlock', 'POST', '{}', publicKey, secretKey);
if (statusCode == 428) {
if (await handleSudoChallenge(responseBody, '/snapshots/${args.snapshotUnlock}/unlock', 'POST', '{}', publicKey, secretKey)) {
print('${green}Snapshot unlocked: ${args.snapshotUnlock}$reset');
} else {
stderr.writeln('${red}Error: Failed to unlock snapshot (OTP verification failed)$reset');
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
print('${green}Snapshot unlocked: ${args.snapshotUnlock}$reset');
} else {
stderr.writeln('${red}Error: Failed to unlock snapshot (HTTP $statusCode)$reset');
exit(1);
}
return;
}
if (args.snapshotClone != null) {
final payload = <String, dynamic>{
'clone_type': args.snapshotCloneType ?? 'session',
};
if (args.snapshotName != null) {
payload['name'] = args.snapshotName;
}
if (args.snapshotPorts != null) {
payload['ports'] = args.snapshotPorts!.split(',').map((p) => int.parse(p.trim())).toList();
}
if (args.snapshotShell != null) {
payload['shell'] = args.snapshotShell;
}
final result = await apiRequestCurl('/snapshots/${args.snapshotClone}/clone', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Snapshot cloned$reset');
print(jsonEncode(result));
return;
}
stderr.writeln('${red}Error: Use --list, --info ID, --session ID, --service ID, --restore ID, --delete ID, --lock ID, --unlock ID, or --clone ID$reset');
exit(1);
}
// Session additional functions
Future<void> sessionInfo(String id, String publicKey, String? secretKey) async {
final result = await apiRequestCurl('/sessions/$id', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
}
Future<void> sessionBoost(String id, int vcpu, String publicKey, String? secretKey) async {
final payload = {'vcpu': vcpu};
await apiRequestCurl('/sessions/$id', 'PATCH', jsonEncode(payload), publicKey, secretKey);
print('${green}Session boosted to $vcpu vCPU$reset');
}
Future<void> sessionUnboost(String id, String publicKey, String? secretKey) async {
final payload = {'vcpu': 1};
await apiRequestCurl('/sessions/$id', 'PATCH', jsonEncode(payload), publicKey, secretKey);
print('${green}Session unboosted to 1 vCPU$reset');
}
Future<void> sessionExecuteCmd(String id, String command, String publicKey, String? secretKey) async {
final payload = {'command': command};
final result = await apiRequestCurl('/sessions/$id/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final stdoutText = result['stdout'] as String?;
final stderrText = result['stderr'] as String?;
if (stdoutText != null && stdoutText.isNotEmpty) {
stdout.write('$blue$stdoutText$reset');
}
if (stderrText != null && stderrText.isNotEmpty) {
stderr.write('$red$stderrText$reset');
}
}
// Service additional functions
Future<void> serviceLock(String id, String publicKey, String? secretKey) async {
await apiRequestCurl('/services/$id/lock', 'POST', '{}', publicKey, secretKey);
print('${green}Service locked: $id$reset');
}
Future<void> serviceUnlock(String id, String publicKey, String? secretKey) async {
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/services/$id/unlock', 'POST', '{}', publicKey, secretKey);
if (statusCode == 428) {
if (await handleSudoChallenge(responseBody, '/services/$id/unlock', 'POST', '{}', publicKey, secretKey)) {
print('${green}Service unlocked: $id$reset');
} else {
stderr.writeln('${red}Error: Failed to unlock service (OTP verification failed)$reset');
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
print('${green}Service unlocked: $id$reset');
} else {
stderr.writeln('${red}Error: Failed to unlock service (HTTP $statusCode)$reset');
exit(1);
}
}
Future<void> serviceRedeploy(String id, String? bootstrap, String publicKey, String? secretKey) async {
final payload = bootstrap != null ? {'bootstrap': bootstrap} : <String, dynamic>{};
await apiRequestCurl('/services/$id/redeploy', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Service redeploying: $id$reset');
}
// PaaS logs functions
Future<void> logsFetch(String source, int lines, String? since, String? grepPattern, String publicKey, String? secretKey) async {
var params = '?source=$source&lines=$lines';
if (since != null) params += '&since=$since';
if (grepPattern != null) params += '&grep=${Uri.encodeComponent(grepPattern)}';
final result = await apiRequestCurl('/logs$params', 'GET', null, publicKey, secretKey);
print(jsonEncode(result));
}
// Utility functions
Future<bool> healthCheck() async {
try {
final result = await Process.run('curl', ['-s', 'https://api.unsandbox.com/health']);
print(result.stdout);
return result.stdout.toString().contains('ok');
} catch (e) {
return false;
}
}
String sdkVersion() {
return '4.2.0';
}
Future<void> cmdKey(Args args) async {
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
final publicKey = keys[0]!;
final secretKey = keys[1];
try {
final result = await apiRequestCurl('/keys/validate', 'POST', null, publicKey, secretKey, baseUrl: portalBase);
// Handle --extend flag
if (args.keyExtend) {
final publicKey = result['public_key'] as String?;
if (publicKey != null) {
final url = '$portalBase/keys/extend?pk=$publicKey';
print('${blue}Opening browser to extend key...$reset');
if (Platform.isMacOS) {
await Process.run('open', [url]);
} else if (Platform.isLinux) {
await Process.run('xdg-open', [url]);
} else if (Platform.isWindows) {
await Process.run('cmd', ['/c', 'start', url]);
} else {
print('${yellow}Please open manually: $url$reset');
}
return;
} else {
stderr.writeln('${red}Error: Could not retrieve public key$reset');
exit(1);
}
}
// Check if key is expired
final expired = result['expired'] as bool? ?? false;
if (expired) {
print('${red}Expired$reset');
print('Public Key: ${result['public_key'] ?? 'N/A'}');
print('Tier: ${result['tier'] ?? 'N/A'}');
print('Expired: ${result['expires_at'] ?? 'N/A'}');
print('${yellow}To renew: Visit $portalBase/keys/extend$reset');
exit(1);
}
// Valid key
print('${green}Valid$reset');
print('Public Key: ${result['public_key'] ?? 'N/A'}');
print('Tier: ${result['tier'] ?? 'N/A'}');
print('Status: ${result['status'] ?? 'N/A'}');
print('Expires: ${result['expires_at'] ?? 'N/A'}');
print('Time Remaining: ${result['time_remaining'] ?? 'N/A'}');
print('Rate Limit: ${result['rate_limit'] ?? 'N/A'}');
print('Burst: ${result['burst'] ?? 'N/A'}');
print('Concurrency: ${result['concurrency'] ?? 'N/A'}');
} catch (e) {
print('${red}Invalid$reset');
print('Reason: $e');
exit(1);
}
}
Args parseArgs(List<String> argv) {
final args = Args();
var i = 0;
while (i < argv.length) {
switch (argv[i]) {
case 'session':
args.command = 'session';
break;
case 'service':
args.command = 'service';
break;
case 'image':
args.command = 'image';
break;
case 'snapshot':
args.command = 'snapshot';
break;
case 'key':
args.command = 'key';
break;
case 'languages':
args.command = 'languages';
break;
case '--json':
if (args.command == 'languages') {
args.languagesJson = true;
}
break;
case '-k':
case '--api-key':
args.apiKey = argv[++i];
break;
case '-p':
case '--public-key':
args.publicKey = argv[++i];
break;
case '--account':
args.account = int.parse(argv[++i]);
break;
case '-n':
case '--network':
args.network = argv[++i];
break;
case '-v':
case '--vcpu':
args.vcpu = int.parse(argv[++i]);
break;
case '-e':
case '--env':
args.env.add(argv[++i]);
break;
case '-f':
case '--files':
args.files.add(argv[++i]);
break;
case '-a':
case '--artifacts':
args.artifacts = true;
break;
case '-o':
case '--output-dir':
args.outputDir = argv[++i];
break;
case '-l':
case '--list':
if (args.command == 'session') {
args.sessionList = true;
} else if (args.command == 'service') {
args.serviceList = true;
} else if (args.command == 'image') {
args.imageList = true;
} else if (args.command == 'snapshot') {
args.snapshotList = true;
}
break;
case '-s':
case '--shell':
if (args.command == 'snapshot') {
args.snapshotShell = argv[++i];
} else {
args.sessionShell = argv[++i];
}
break;
case '--kill':
args.sessionKill = argv[++i];
break;
case '--name':
if (args.command == 'image') {
args.imageName = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotName = argv[++i];
} else {
args.serviceName = argv[++i];
}
break;
case '--ports':
if (args.command == 'image') {
args.imagePorts = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotPorts = argv[++i];
} else {
args.servicePorts = argv[++i];
}
break;
case '--type':
args.serviceType = argv[++i];
break;
case '--bootstrap':
args.serviceBootstrap = argv[++i];
break;
case '--bootstrap-file':
args.serviceBootstrapFile = argv[++i];
break;
// --info handled below in the image command section
case '--logs':
args.serviceLogs = argv[++i];
break;
case '--tail':
args.serviceTail = argv[++i];
break;
case '--freeze':
args.serviceSleep = argv[++i];
break;
case '--unfreeze':
args.serviceWake = argv[++i];
break;
case '--destroy':
args.serviceDestroy = argv[++i];
break;
case '--resize':
args.serviceResize = argv[++i];
break;
case '--vcpu':
args.serviceResizeVcpu = int.parse(argv[++i]);
break;
case '--execute':
args.serviceExecute = argv[++i];
break;
case '--command':
args.serviceCommand = argv[++i];
break;
case '--dump-bootstrap':
args.serviceDumpBootstrap = argv[++i];
break;
case '--dump-file':
args.serviceDumpFile = argv[++i];
break;
case '--unfreeze-on-demand':
args.serviceUnfreezeOnDemand = argv[++i];
break;
case '--unfreeze-on-demand-enabled':
args.serviceUnfreezeOnDemandEnabled = argv[++i].toLowerCase() == 'true';
break;
case '--with-unfreeze-on-demand':
args.serviceCreateUnfreezeOnDemand = true;
break;
case '--show-freeze-page':
args.serviceShowFreezePage = argv[++i];
break;
case '--show-freeze-page-enabled':
args.serviceShowFreezePageEnabled = argv[++i].toLowerCase() == 'true';
break;
case '--extend':
args.keyExtend = true;
break;
case '--env-file':
args.envFile = argv[++i];
break;
case '--info':
if (args.command == 'service') {
args.serviceInfo = argv[++i];
} else if (args.command == 'image') {
args.imageInfo = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotInfo = argv[++i];
}
break;
case '--delete':
if (args.command == 'image') {
args.imageDelete = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotDelete = argv[++i];
}
break;
case '--lock':
if (args.command == 'image') {
args.imageLock = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotLock = argv[++i];
}
break;
case '--unlock':
if (args.command == 'image') {
args.imageUnlock = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotUnlock = argv[++i];
}
break;
case '--publish':
if (args.command == 'image') {
args.imagePublish = argv[++i];
}
break;
case '--source-type':
args.imageSourceType = argv[++i];
break;
case '--visibility':
if (args.command == 'image') {
args.imageVisibility = argv[++i];
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
args.imageVisibilityMode = argv[++i];
}
}
break;
case '--spawn':
if (args.command == 'image') {
args.imageSpawn = argv[++i];
}
break;
case '--clone':
if (args.command == 'image') {
args.imageClone = argv[++i];
} else if (args.command == 'snapshot') {
args.snapshotClone = argv[++i];
}
break;
case '--session':
if (args.command == 'snapshot') {
args.snapshotSession = argv[++i];
}
break;
case '--service':
if (args.command == 'snapshot') {
args.snapshotService = argv[++i];
}
break;
case '--restore':
if (args.command == 'snapshot') {
args.snapshotRestore = argv[++i];
}
break;
case '--clone-type':
if (args.command == 'snapshot') {
args.snapshotCloneType = argv[++i];
}
break;
case '--hot':
if (args.command == 'snapshot') {
args.snapshotHot = true;
}
break;
case 'env':
if (args.command == 'service' && i + 1 < argv.length) {
args.envAction = argv[++i];
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
args.envTarget = argv[++i];
}
}
break;
default:
if (argv[i].startsWith('-')) {
stderr.writeln('${red}Unknown option: ${argv[i]}$reset');
exit(1);
} else {
args.sourceFile = argv[i];
}
}
i++;
}
return args;
}
void printHelp() {
print('''
Usage: dart un.dart [options] <source_file>
dart un.dart session [options]
dart un.dart service [options]
dart un.dart snapshot [options]
dart un.dart image [options]
dart un.dart key [options]
dart un.dart languages [--json]
Execute options:
-e KEY=VALUE Set environment variable
-f FILE Add input file
-a Return artifacts
-o DIR Output directory for artifacts
-n MODE Network mode (zerotrust/semitrusted)
-v N vCPU count (1-8)
-p KEY Public key (use with -k for secret key)
-k KEY Secret/API key
--account N Use row N from accounts.csv (0-based)
Session options:
--list List active sessions
--shell NAME Shell/REPL to use
--kill ID Terminate session
Service options:
--list List services
--name NAME Service name
--ports PORTS Comma-separated ports
--type TYPE Service type (minecraft/mumble/teamspeak/source/tcp/udp)
--bootstrap CMD Bootstrap command
-e KEY=VALUE Environment variable for vault
--env-file FILE Load vault variables from file
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--unfreeze-on-demand ID Set unfreeze-on-demand for service
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
--show-freeze-page ID Set show-freeze-page for service
--show-freeze-page-enabled BOOL Enable/disable (default: true)
--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)
Service env commands:
env status ID Show vault status
env set ID Set vault (-e KEY=VALUE or --env-file FILE)
env export ID Export vault contents
env delete ID Delete vault
Image options:
-l, --list List all images
--info ID Get image details
--delete ID Delete an image
--lock ID Lock image to prevent deletion
--unlock ID Unlock image
--publish ID Publish image from service/snapshot (requires --source-type)
--source-type TYPE Source type: service or snapshot
--visibility ID MODE Set visibility: private, unlisted, or public
--spawn ID Spawn new service from image
--clone ID Clone an image
--name NAME Name for spawned service or cloned image
--ports PORTS Ports for spawned service
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--session ID Create snapshot from session
--service ID Create snapshot from service
--restore ID Restore a snapshot
--delete ID Delete a snapshot
--lock ID Lock snapshot to prevent deletion
--unlock ID Unlock snapshot
--clone ID Clone snapshot to session/service
--clone-type TYPE Clone target: session (default) or service
--name NAME Name for new snapshot or cloned resource
--ports PORTS Ports for service (with --clone --clone-type service)
--shell NAME Shell for session (with --clone --clone-type session)
--hot Hot snapshot (without stopping)
Key options:
--extend Open browser to extend key
Languages options:
--json Output as JSON array
''');
}
void main(List<String> arguments) async {
try {
final args = parseArgs(arguments);
if (args.command == 'session') {
await cmdSession(args);
} else if (args.command == 'service') {
await cmdService(args);
} else if (args.command == 'image') {
await cmdImage(args);
} else if (args.command == 'snapshot') {
await cmdSnapshot(args);
} else if (args.command == 'key') {
await cmdKey(args);
} else if (args.command == 'languages') {
await cmdLanguages(args);
} else if (args.sourceFile != null) {
await cmdExecute(args);
} else {
printHelp();
exit(1);
}
} catch (e) {
stderr.writeln('${red}Error: $e$reset');
exit(1);
}
}
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software