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 — TypeScript
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/typescript/sync/src/un.ts && chmod +x un.ts && ln -sf un.ts 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.ts
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 TypeScript existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/typescript/sync/src/un.ts
# 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 TypeScript app:
import { executeCode } from './un';
const result = await executeCode("typescript", "console.log('Hello from TypeScript running on unsandbox!')");
console.log(result.stdout); // Hello from TypeScript running on unsandbox!
npx ts-node myapp.ts
97146879f2274a65146f308c2ec1f3bd
SHA256: 68fd5f239b5a5ebf43ba490eb2801742536ec73819f654f415582fbe5dd74b8f
#!/usr/bin/env ts-node
// 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.ts - Unsandbox CLI Client (TypeScript 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.ts [options] <source_file>
* un.ts session [options]
* un.ts service [options]
*
* Requires: UNSANDBOX_API_KEY environment variable
*/
import * as fs from 'fs';
import * as https from 'https';
import * as path from 'path';
import * as crypto from 'crypto';
const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com";
const LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
const SDK_VERSION = "4.2.0";
// Thread-local error storage
let _lastError: string | null = null;
// =============================================================================
// Exported Utility Functions (for library usage)
// =============================================================================
/**
* Get the SDK version string.
*/
export function version(): string {
return SDK_VERSION;
}
/**
* Get the last error message.
*/
export function lastError(): string | null {
return _lastError;
}
/**
* Sign a message using HMAC-SHA256.
* Exposed for testing and debugging purposes.
*/
export function hmacSign(secretKey: string, message: string): string {
return crypto.createHmac('sha256', secretKey).update(message).digest('hex');
}
/**
* Check if the API is healthy and responding.
*/
export async function healthCheck(): Promise<boolean> {
return new Promise((resolve) => {
const url = new URL(`${API_BASE}/health`);
const req = https.get(url, (res) => {
resolve(res.statusCode === 200);
});
req.on('error', (e) => {
_lastError = `Health check failed: ${e.message}`;
resolve(false);
});
req.setTimeout(10000, () => {
_lastError = 'Health check failed: timeout';
resolve(false);
});
});
}
const BLUE = "\x1b[34m";
const RED = "\x1b[31m";
const GREEN = "\x1b[32m";
const YELLOW = "\x1b[33m";
const RESET = "\x1b[0m";
const EXT_MAP: Record<string, string> = {
".py": "python", ".js": "javascript", ".ts": "typescript",
".rb": "ruby", ".php": "php", ".pl": "perl", ".lua": "lua",
".sh": "bash", ".go": "go", ".rs": "rust", ".c": "c",
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
".java": "java", ".kt": "kotlin", ".cs": "csharp", ".fs": "fsharp",
".hs": "haskell", ".ml": "ocaml", ".clj": "clojure", ".scm": "scheme",
".lisp": "commonlisp", ".erl": "erlang", ".ex": "elixir", ".exs": "elixir",
".jl": "julia", ".r": "r", ".R": "r", ".cr": "crystal",
".d": "d", ".nim": "nim", ".zig": "zig", ".v": "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",
};
interface Args {
command: string | null;
sourceFile: string | null;
env: string[];
files: string[];
artifacts: boolean;
outputDir: string | null;
network: string | null;
vcpu: number | null;
apiKey: string | null;
publicKey: string | null;
account: number | null;
shell: string | null;
list: boolean;
attach: string | null;
kill: string | null;
audit: boolean;
tmux: boolean;
screen: boolean;
name: string | null;
ports: string | null;
domains: string | null;
type: string | null;
bootstrap: string | null;
bootstrapFile: string | null;
info: string | null;
logs: string | null;
tail: string | null;
sleep: string | null;
wake: string | null;
destroy: string | null;
resize: string | null;
execute: string | null;
command_arg: string | null;
extend: boolean;
snapshot: string | null;
restore: string | null;
from: string | null;
snapshotName: string | null;
hot: boolean;
deleteSnapshot: string | null;
clone: string | null;
cloneType: string | null;
cloneName: string | null;
cloneShell: string | null;
clonePorts: string | null;
dumpBootstrap: string | null;
dumpFile: string | null;
envFile: string | null;
envAction: string | null;
envTarget: string | null;
jsonOutput: boolean;
imageInfo: string | null;
imageDelete: string | null;
imageLock: string | null;
imageUnlock: string | null;
imagePublish: string | null;
sourceType: string | null;
imageVisibility: string | null;
visibilityMode: string | null;
imageSpawn: string | null;
imageClone: string | null;
unfreezeOnDemand: boolean | null;
setUnfreezeOnDemand: string | null;
showFreezePage: boolean | null;
setShowFreezePage: string | null;
redeploy: string | null;
}
interface ApiKeys {
publicKey: string;
secretKey: string;
}
function loadAccountsCSV(filePath: string, index: number): { pk: string; sk: string } | null {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n').filter(l => {
const t = l.trim();
return t.length > 0 && !t.startsWith('#');
});
if (index < 0 || index >= lines.length) return null;
const parts = lines[index].split(',');
if (parts.length < 2) return null;
const pk = parts[0].trim();
const sk = parts[1].trim();
if (!pk || !sk) return null;
return { pk, sk };
} catch (e) {
return null;
}
}
function getApiKeys(argsPublicKey: string | null, argsSecretKey: string | null, account: number | null): ApiKeys {
// Tier 1: explicit -p/-k flags
if (argsPublicKey && argsSecretKey) {
return { publicKey: argsPublicKey, secretKey: argsSecretKey };
}
// Tier 2: --account N → accounts.csv row N (bypasses env vars)
if (account !== null) {
const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
const homeCsv = path.join(homeDir, '.unsandbox', 'accounts.csv');
const localCsv = './accounts.csv';
const fromHome = loadAccountsCSV(homeCsv, account);
if (fromHome) return { publicKey: fromHome.pk, secretKey: fromHome.sk };
const fromLocal = loadAccountsCSV(localCsv, account);
if (fromLocal) return { publicKey: fromLocal.pk, secretKey: fromLocal.sk };
console.error(`${RED}Error: --account ${account} not found in accounts.csv${RESET}`);
process.exit(1);
}
// Tier 3: env vars
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
if (publicKey && secretKey) {
return { publicKey, secretKey };
}
// Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
const accountIndex = process.env.UNSANDBOX_ACCOUNT ? parseInt(process.env.UNSANDBOX_ACCOUNT) : 0;
const homeCsv = path.join(homeDir, '.unsandbox', 'accounts.csv');
const fromHome = loadAccountsCSV(homeCsv, accountIndex);
if (fromHome) return { publicKey: fromHome.pk, secretKey: fromHome.sk };
// Tier 5: ./accounts.csv row 0
const fromLocal = loadAccountsCSV('./accounts.csv', accountIndex);
if (fromLocal) return { publicKey: fromLocal.pk, secretKey: fromLocal.sk };
// Legacy UNSANDBOX_API_KEY fallback
const oldKey = process.env.UNSANDBOX_API_KEY;
if (oldKey) {
return { publicKey: oldKey, secretKey: oldKey };
}
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
process.exit(1);
}
function detectLanguage(filename: string): string {
const ext = path.extname(filename).toLowerCase();
const lang = EXT_MAP[ext];
if (!lang) {
try {
const firstLine = fs.readFileSync(filename, 'utf-8').split('\n')[0];
if (firstLine.startsWith('#!')) {
if (firstLine.includes('python')) return 'python';
if (firstLine.includes('node')) return 'javascript';
if (firstLine.includes('ruby')) return 'ruby';
if (firstLine.includes('perl')) return 'perl';
if (firstLine.includes('bash') || firstLine.includes('/sh')) return 'bash';
if (firstLine.includes('lua')) return 'lua';
if (firstLine.includes('php')) return 'php';
}
} catch (e) {}
console.error(`${RED}Error: Cannot detect language for ${filename}${RESET}`);
process.exit(1);
}
return lang;
}
function apiRequest(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(API_BASE + endpoint);
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
const options: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 300000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(body));
} catch (e) {
resolve(body);
}
} else {
if (res.statusCode === 401 && body.toLowerCase().includes('timestamp')) {
console.error(`${RED}Error: Request timestamp expired (must be within 5 minutes of server time)${RESET}`);
console.error(`${YELLOW}Your computer's clock may have drifted.${RESET}`);
console.error("Check your system time and sync with NTP if needed:");
console.error(" Linux: sudo ntpdate -s time.nist.gov");
console.error(" macOS: sudo sntp -sS time.apple.com");
console.error(" Windows: w32tm /resync");
} else {
console.error(`${RED}Error: HTTP ${res.statusCode} - ${body}${RESET}`);
}
process.exit(1);
}
});
});
req.on('error', (e) => {
console.error(`${RED}Error: ${e.message}${RESET}`);
process.exit(1);
});
if (data) {
req.write(body);
}
req.end();
});
}
/**
* Make an authenticated HTTP request with sudo OTP challenge handling.
* If the server returns 428, prompts for OTP and retries with sudo headers.
*/
function apiRequestWithSudo(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(API_BASE + endpoint);
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
const options: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 300000
};
const req = https.request(options, (res) => {
let responseBody = '';
res.on('data', chunk => responseBody += chunk);
res.on('end', async () => {
// Handle 428 sudo OTP challenge
if (res.statusCode === 428) {
let challengeId = '';
try {
const challengeData = JSON.parse(responseBody);
challengeId = challengeData.challenge_id || '';
} catch (e) {
// Ignore JSON parse errors
}
console.error(`${YELLOW}Confirmation required. Check your email for a one-time code.${RESET}`);
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stderr
});
rl.question('Enter OTP: ', (otp: string) => {
rl.close();
otp = otp.trim();
if (!otp) {
console.error(`${RED}Error: Operation cancelled${RESET}`);
process.exit(1);
}
// Retry with sudo headers
const retryTimestamp = Math.floor(Date.now() / 1000).toString();
const retryMessage = `${retryTimestamp}:${method}:${url.pathname}${url.search}:${body}`;
const retrySignature = crypto.createHmac('sha256', keys.secretKey).update(retryMessage).digest('hex');
const retryOptions: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': retryTimestamp,
'X-Signature': retrySignature,
'Content-Type': 'application/json',
'X-Sudo-OTP': otp,
'X-Sudo-Challenge': challengeId
},
timeout: 300000
};
const retryReq = https.request(retryOptions, (retryRes) => {
let retryBody = '';
retryRes.on('data', chunk => retryBody += chunk);
retryRes.on('end', () => {
if (retryRes.statusCode && retryRes.statusCode >= 200 && retryRes.statusCode < 300) {
try {
resolve(JSON.parse(retryBody));
} catch (e) {
resolve(retryBody);
}
} else {
console.error(`${RED}Error: HTTP ${retryRes.statusCode} - ${retryBody}${RESET}`);
process.exit(1);
}
});
});
retryReq.on('error', (e) => {
console.error(`${RED}Error: ${e.message}${RESET}`);
process.exit(1);
});
if (data) {
retryReq.write(body);
}
retryReq.end();
});
return;
}
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(responseBody));
} catch (e) {
resolve(responseBody);
}
} else {
if (res.statusCode === 401 && responseBody.toLowerCase().includes('timestamp')) {
console.error(`${RED}Error: Request timestamp expired (must be within 5 minutes of server time)${RESET}`);
console.error(`${YELLOW}Your computer's clock may have drifted.${RESET}`);
console.error("Check your system time and sync with NTP if needed:");
console.error(" Linux: sudo ntpdate -s time.nist.gov");
console.error(" macOS: sudo sntp -sS time.apple.com");
console.error(" Windows: w32tm /resync");
} else {
console.error(`${RED}Error: HTTP ${res.statusCode} - ${responseBody}${RESET}`);
}
process.exit(1);
}
});
});
req.on('error', (e) => {
console.error(`${RED}Error: ${e.message}${RESET}`);
process.exit(1);
});
if (data) {
req.write(body);
}
req.end();
});
}
function portalRequest(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(PORTAL_BASE + endpoint);
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
const options: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 30000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(body);
resolve(parsed);
} catch (e) {
resolve({ error: body, status: res.statusCode });
}
});
});
req.on('error', (e) => {
reject(e);
});
if (data) {
req.write(body);
}
req.end();
});
}
function apiRequestText(endpoint: string, method: string, body: string, keys: ApiKeys): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(API_BASE + endpoint);
const timestamp = Math.floor(Date.now() / 1000).toString();
const message = `${timestamp}:${method}:${url.pathname}:${body}`;
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
const options: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname,
method: method,
headers: {
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'text/plain'
},
timeout: 300000
};
const req = https.request(options, (res) => {
let responseBody = '';
res.on('data', chunk => responseBody += chunk);
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(responseBody));
} catch (e) {
resolve({ error: responseBody });
}
} else {
resolve({ error: `HTTP ${res.statusCode} - ${responseBody}` });
}
});
});
req.on('error', (e) => {
resolve({ error: e.message });
});
req.write(body);
req.end();
});
}
// ============================================================================
// Environment Secrets Vault Functions
// ============================================================================
const MAX_ENV_CONTENT_SIZE = 64 * 1024; // 64KB max
async function serviceEnvStatus(serviceId: string, keys: ApiKeys): Promise<void> {
const result = await apiRequest(`/services/${serviceId}/env`, "GET", null, keys);
const hasVault = result.has_vault;
if (!hasVault) {
console.log("Vault exists: no");
console.log("Variable count: 0");
} else {
console.log("Vault exists: yes");
console.log(`Variable count: ${result.count || 0}`);
if (result.updated_at) {
const date = new Date(result.updated_at * 1000);
console.log(`Last updated: ${date.toISOString().replace('T', ' ').split('.')[0]}`);
}
}
}
async function serviceEnvSet(serviceId: string, envContent: string, keys: ApiKeys): Promise<boolean> {
if (!envContent) {
console.error(`${RED}Error: No environment content provided${RESET}`);
return false;
}
if (envContent.length > MAX_ENV_CONTENT_SIZE) {
console.error(`${RED}Error: Environment content too large (max ${MAX_ENV_CONTENT_SIZE} bytes)${RESET}`);
return false;
}
const result = await apiRequestText(`/services/${serviceId}/env`, "PUT", envContent, keys);
if (result.error) {
console.error(`${RED}Error: ${result.error}${RESET}`);
return false;
}
const count = result.count || 0;
const plural = count === 1 ? '' : 's';
console.log(`${GREEN}Environment vault updated: ${count} variable${plural}${RESET}`);
if (result.message) console.log(result.message);
return true;
}
async function serviceEnvExport(serviceId: string, keys: ApiKeys): Promise<void> {
const result = await apiRequest(`/services/${serviceId}/env/export`, "POST", {}, keys);
const envContent = result.env || '';
if (envContent) {
process.stdout.write(envContent);
if (!envContent.endsWith('\n')) console.log();
}
}
async function serviceEnvDelete(serviceId: string, keys: ApiKeys): Promise<void> {
await apiRequest(`/services/${serviceId}/env`, "DELETE", null, keys);
console.log(`${GREEN}Environment vault deleted${RESET}`);
}
function readEnvFile(filepath: string): string {
try {
return fs.readFileSync(filepath, 'utf-8');
} catch (e) {
console.error(`${RED}Error: Env file not found: ${filepath}${RESET}`);
process.exit(1);
}
}
function getLanguagesCachePath(): string {
const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
return path.join(homeDir, '.unsandbox', 'languages.json');
}
function loadLanguagesCache(): string[] | null {
try {
const cachePath = getLanguagesCachePath();
if (!fs.existsSync(cachePath)) {
return null;
}
const stat = fs.statSync(cachePath);
const ageSeconds = (Date.now() - stat.mtimeMs) / 1000;
if (ageSeconds >= LANGUAGES_CACHE_TTL) {
return null;
}
const data = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
return data.languages || null;
} catch (e) {
return null;
}
}
function saveLanguagesCache(languages: string[]): void {
try {
const cachePath = getLanguagesCachePath();
const cacheDir = path.dirname(cachePath);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
const data = { languages: languages, timestamp: Math.floor(Date.now() / 1000) };
fs.writeFileSync(cachePath, JSON.stringify(data));
} catch (e) {
// Cache failures are non-fatal
}
}
function buildEnvContent(envs: string[], envFile: string | null): string {
const parts: string[] = [];
// Read from env file first
if (envFile) {
parts.push(readEnvFile(envFile));
}
// Add -e flags
envs.forEach(e => {
if (e.includes('=')) {
parts.push(e);
}
});
return parts.join('\n');
}
async function cmdServiceEnv(action: string, target: string, envs: string[], envFile: string | null, keys: ApiKeys): Promise<void> {
if (!action) {
console.error(`${RED}Error: env action required (status, set, export, delete)${RESET}`);
process.exit(1);
}
if (!target) {
console.error(`${RED}Error: Service ID required for env command${RESET}`);
process.exit(1);
}
switch (action) {
case 'status':
await serviceEnvStatus(target, keys);
break;
case 'set':
const envContent = buildEnvContent(envs, envFile);
if (!envContent) {
console.error(`${RED}Error: No env content provided. Use -e KEY=VAL or --env-file${RESET}`);
process.exit(1);
}
await serviceEnvSet(target, envContent, keys);
break;
case 'export':
await serviceEnvExport(target, keys);
break;
case 'delete':
await serviceEnvDelete(target, keys);
break;
default:
console.error(`${RED}Error: Unknown env action '${action}'. Use: status, set, export, delete${RESET}`);
process.exit(1);
}
}
async function cmdExecute(args: Args): Promise<void> {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
let code: string;
try {
code = fs.readFileSync(args.sourceFile!, 'utf-8');
} catch (e) {
console.error(`${RED}Error: File not found: ${args.sourceFile}${RESET}`);
process.exit(1);
}
const language = detectLanguage(args.sourceFile!);
const payload: any = { language, code };
if (args.env && args.env.length > 0) {
payload.env = {};
args.env.forEach(e => {
const idx = e.indexOf('=');
if (idx > 0) {
payload.env[e.substring(0, idx)] = e.substring(idx + 1);
}
});
}
if (args.files && args.files.length > 0) {
payload.input_files = args.files.map(filepath => {
try {
const content = fs.readFileSync(filepath);
return {
filename: path.basename(filepath),
content_base64: content.toString('base64')
};
} catch (e) {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
process.exit(1);
}
});
}
if (args.artifacts) payload.return_artifacts = true;
if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu;
const result = await apiRequest("/execute", "POST", payload, keys);
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
if (args.artifacts && result.artifacts) {
const outDir = args.outputDir || '.';
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
result.artifacts.forEach((artifact: any) => {
const filename = artifact.filename || 'artifact';
const content = Buffer.from(artifact.content_base64, 'base64');
const filepath = path.join(outDir, filename);
fs.writeFileSync(filepath, content);
fs.chmodSync(filepath, 0o755);
console.error(`${GREEN}Saved: ${filepath}${RESET}`);
});
}
process.exit(result.exit_code || 0);
}
async function cmdSession(args: Args): Promise<void> {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
if (args.list) {
const result = await apiRequest("/sessions", "GET", null, keys);
const sessions = result.sessions || [];
if (sessions.length === 0) {
console.log("No active sessions");
} else {
console.log(`${'ID'.padEnd(40)} ${'Shell'.padEnd(10)} ${'Status'.padEnd(10)} Created`);
sessions.forEach((s: any) => {
console.log(`${(s.id || 'N/A').padEnd(40)} ${(s.shell || 'N/A').padEnd(10)} ${(s.status || 'N/A').padEnd(10)} ${s.created_at || 'N/A'}`);
});
}
return;
}
if (args.kill) {
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, keys);
console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`);
return;
}
if (args.attach) {
console.log(`${YELLOW}Attaching to session ${args.attach}...${RESET}`);
console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
return;
}
const payload: any = { shell: args.shell || "bash" };
if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu;
if (args.tmux) payload.persistence = "tmux";
if (args.screen) payload.persistence = "screen";
if (args.audit) payload.audit = true;
// Add input files
if (args.files && args.files.length > 0) {
payload.input_files = args.files.map(filepath => {
try {
const content = fs.readFileSync(filepath);
return {
filename: path.basename(filepath),
content_base64: content.toString('base64')
};
} catch (e) {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
process.exit(1);
}
});
}
console.log(`${YELLOW}Creating session...${RESET}`);
const result = await apiRequest("/sessions", "POST", payload, keys);
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
}
async function cmdService(args: Args): Promise<void> {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
if (args.list) {
const result = await apiRequest("/services", "GET", null, keys);
const services = result.services || [];
if (services.length === 0) {
console.log("No services");
} else {
console.log(`${'ID'.padEnd(20)} ${'Name'.padEnd(15)} ${'Status'.padEnd(10)} ${'Ports'.padEnd(15)} Domains`);
services.forEach((s: any) => {
const ports = (s.ports || []).join(',');
const domains = (s.domains || []).join(',');
console.log(`${(s.id || 'N/A').padEnd(20)} ${(s.name || 'N/A').padEnd(15)} ${(s.status || 'N/A').padEnd(10)} ${ports.padEnd(15)} ${domains}`);
});
}
return;
}
if (args.info) {
const result = await apiRequest(`/services/${args.info}`, "GET", null, keys);
console.log(JSON.stringify(result, null, 2));
return;
}
if (args.logs) {
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, keys);
console.log(result.logs || "");
return;
}
if (args.tail) {
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, keys);
console.log(result.logs || "");
return;
}
if (args.sleep) {
await apiRequest(`/services/${args.sleep}/freeze`, "POST", null, keys);
console.log(`${GREEN}Service frozen: ${args.sleep}${RESET}`);
return;
}
if (args.wake) {
await apiRequest(`/services/${args.wake}/unfreeze`, "POST", null, keys);
console.log(`${GREEN}Service unfreezing: ${args.wake}${RESET}`);
return;
}
if (args.destroy) {
await apiRequestWithSudo(`/services/${args.destroy}`, "DELETE", null, keys);
console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`);
return;
}
if (args.resize) {
if (!args.vcpu) {
console.error(`${RED}Error: --vcpu required with --resize${RESET}`);
process.exit(1);
}
const payload = { vcpu: args.vcpu };
await apiRequest(`/services/${args.resize}`, "PATCH", payload, keys);
console.log(`${GREEN}Service resized to ${args.vcpu} vCPU, ${args.vcpu * 2}GB RAM${RESET}`);
return;
}
if (args.setUnfreezeOnDemand) {
const enabled = args.unfreezeOnDemand === true;
const payload = { unfreeze_on_demand: enabled };
await apiRequest(`/services/${args.setUnfreezeOnDemand}`, "PATCH", payload, keys);
console.log(`${GREEN}Unfreeze on demand ${enabled ? 'enabled' : 'disabled'} for service ${args.setUnfreezeOnDemand}${RESET}`);
return;
}
if (args.setShowFreezePage) {
const enabled = args.showFreezePage === true;
const payload = { show_freeze_page: enabled };
await apiRequest(`/services/${args.setShowFreezePage}`, "PATCH", payload, keys);
console.log(`${GREEN}Show freeze page ${enabled ? 'enabled' : 'disabled'} for service ${args.setShowFreezePage}${RESET}`);
return;
}
if (args.redeploy) {
const payload: any = {};
if (args.bootstrap) {
payload.bootstrap = args.bootstrap;
}
if (args.bootstrapFile) {
if (!fs.existsSync(args.bootstrapFile)) {
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
process.exit(1);
}
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
}
if (args.files && args.files.length > 0) {
payload.input_files = args.files.map(filepath => {
try {
const content = fs.readFileSync(filepath);
return {
filename: path.basename(filepath),
content_base64: content.toString('base64')
};
} catch (e) {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
process.exit(1);
}
});
}
await apiRequest(`/services/${args.redeploy}/redeploy`, "POST", payload, keys);
console.log(`${GREEN}Service redeployed: ${args.redeploy}${RESET}`);
return;
}
if (args.execute) {
const payload = { command: args.command_arg };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
return;
}
if (args.dumpBootstrap) {
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
const payload = { command: "cat /tmp/bootstrap.sh" };
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, keys);
if (result.stdout) {
const bootstrap = result.stdout;
if (args.dumpFile) {
// Write to file
try {
fs.writeFileSync(args.dumpFile, bootstrap);
fs.chmodSync(args.dumpFile, 0o755);
console.log(`Bootstrap saved to ${args.dumpFile}`);
} catch (e: any) {
console.error(`${RED}Error: Could not write to ${args.dumpFile}: ${e.message}${RESET}`);
process.exit(1);
}
} else {
// Print to stdout
process.stdout.write(bootstrap);
}
} else {
console.error(`${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}`);
process.exit(1);
}
return;
}
if (args.name) {
const payload: any = { name: args.name };
if (args.ports) payload.ports = args.ports.split(',').map(p => parseInt(p.trim()));
if (args.domains) payload.domains = args.domains.split(',');
if (args.type) payload.service_type = args.type;
if (args.bootstrap) {
payload.bootstrap = args.bootstrap;
}
if (args.bootstrapFile) {
if (!fs.existsSync(args.bootstrapFile)) {
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
process.exit(1);
}
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
}
// Add input files
if (args.files && args.files.length > 0) {
payload.input_files = args.files.map(filepath => {
try {
const content = fs.readFileSync(filepath);
return {
filename: path.basename(filepath),
content_base64: content.toString('base64')
};
} catch (e) {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
process.exit(1);
}
});
}
if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu;
if (args.unfreezeOnDemand) payload.unfreeze_on_demand = true;
const result = await apiRequest("/services", "POST", payload, keys);
const serviceId = result.id;
console.log(`${GREEN}Service created: ${serviceId || 'N/A'}${RESET}`);
console.log(`Name: ${result.name || 'N/A'}`);
if (result.url) console.log(`URL: ${result.url}`);
// Auto-set vault if -e or --env-file provided
const envContent = buildEnvContent(args.env || [], args.envFile);
if (envContent && serviceId) {
await serviceEnvSet(serviceId, envContent, keys);
}
return;
}
console.error(`${RED}Error: Specify --name to create a service, or use --list, --info, etc.${RESET}`);
process.exit(1);
}
function openBrowser(url: string): void {
const { exec } = require('child_process');
const platform = process.platform;
let command: string;
if (platform === 'darwin') {
command = `open "${url}"`;
} else if (platform === 'win32') {
command = `start "${url}"`;
} else {
command = `xdg-open "${url}"`;
}
exec(command, (error: any) => {
if (error) {
console.error(`${RED}Error opening browser: ${error.message}${RESET}`);
console.log(`Please visit: ${url}`);
}
});
}
async function validateKey(keys: ApiKeys, shouldExtend: boolean): Promise<void> {
try {
const result = await portalRequest("/keys/validate", "POST", {}, keys);
// Handle --extend flag first
if (shouldExtend) {
const public_key = result.public_key;
if (public_key) {
const extendUrl = `${PORTAL_BASE}/keys/extend?pk=${encodeURIComponent(public_key)}`;
console.log(`${BLUE}Opening browser to extend key...${RESET}`);
openBrowser(extendUrl);
return;
} else {
console.error(`${RED}Error: Could not retrieve public key${RESET}`);
process.exit(1);
}
}
// Check if key is expired
if (result.expired) {
console.log(`${RED}Expired${RESET}`);
console.log(`Public Key: ${result.public_key || 'N/A'}`);
console.log(`Tier: ${result.tier || 'N/A'}`);
console.log(`Expired: ${result.expires_at || 'N/A'}`);
console.log(`${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}`);
process.exit(1);
}
// Valid key
console.log(`${GREEN}Valid${RESET}`);
console.log(`Public Key: ${result.public_key || 'N/A'}`);
console.log(`Tier: ${result.tier || 'N/A'}`);
console.log(`Status: ${result.status || 'N/A'}`);
console.log(`Expires: ${result.expires_at || 'N/A'}`);
console.log(`Time Remaining: ${result.time_remaining || 'N/A'}`);
console.log(`Rate Limit: ${result.rate_limit || 'N/A'}`);
console.log(`Burst: ${result.burst || 'N/A'}`);
console.log(`Concurrency: ${result.concurrency || 'N/A'}`);
} catch (error: any) {
console.error(`${RED}Error validating key: ${error.message}${RESET}`);
process.exit(1);
}
}
async function cmdLanguages(args: Args): Promise<void> {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
// Try cache first
let langs = loadLanguagesCache();
if (!langs) {
const result = await apiRequest("/languages", "GET", null, keys);
langs = result.languages || [];
// Save to cache
saveLanguagesCache(langs);
}
if (args.jsonOutput) {
// JSON array output
console.log(JSON.stringify(langs));
} else {
// One language per line (default)
langs.forEach((lang: string) => {
console.log(lang);
});
}
}
async function cmdImage(args: Args): Promise<void> {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
if (args.list) {
const result = await apiRequest("/images", "GET", null, keys);
const images = result.images || [];
if (images.length === 0) {
console.log("No images found");
} else {
console.log(`${'ID'.padEnd(40)} ${'Name'.padEnd(20)} ${'Visibility'.padEnd(12)} Created`);
images.forEach((img: any) => {
console.log(`${(img.id || 'N/A').padEnd(40)} ${(img.name || '-').padEnd(20)} ${(img.visibility || 'N/A').padEnd(12)} ${img.created_at || 'N/A'}`);
});
}
return;
}
if (args.imageInfo) {
const result = await apiRequest(`/images/${args.imageInfo}`, "GET", null, keys);
console.log(`${BLUE}Image Details${RESET}`);
console.log("");
console.log(`Image ID: ${result.id || 'N/A'}`);
console.log(`Name: ${result.name || '-'}`);
console.log(`Visibility: ${result.visibility || 'N/A'}`);
console.log(`Created: ${result.created_at || 'N/A'}`);
return;
}
if (args.imageDelete) {
await apiRequestWithSudo(`/images/${args.imageDelete}`, "DELETE", null, keys);
console.log(`${GREEN}Image deleted successfully${RESET}`);
return;
}
if (args.imageLock) {
await apiRequest(`/images/${args.imageLock}/lock`, "POST", {}, keys);
console.log(`${GREEN}Image locked successfully${RESET}`);
return;
}
if (args.imageUnlock) {
await apiRequestWithSudo(`/images/${args.imageUnlock}/unlock`, "POST", {}, keys);
console.log(`${GREEN}Image unlocked successfully${RESET}`);
return;
}
if (args.imagePublish) {
if (!args.sourceType) {
console.error(`${RED}Error: --source-type required for --publish (service or snapshot)${RESET}`);
process.exit(1);
}
const payload: any = { source_type: args.sourceType, source_id: args.imagePublish };
if (args.name) payload.name = args.name;
const result = await apiRequest("/images/publish", "POST", payload, keys);
console.log(`${GREEN}Image published successfully${RESET}`);
console.log(`Image ID: ${result.id || 'N/A'}`);
return;
}
if (args.imageVisibility) {
if (!args.visibilityMode) {
console.error(`${RED}Error: visibility mode required (private, unlisted, or public)${RESET}`);
process.exit(1);
}
const payload = { visibility: args.visibilityMode };
await apiRequest(`/images/${args.imageVisibility}/visibility`, "POST", payload, keys);
console.log(`${GREEN}Image visibility set to ${args.visibilityMode}${RESET}`);
return;
}
if (args.imageSpawn) {
const payload: any = {};
if (args.name) payload.name = args.name;
if (args.ports) payload.ports = args.ports.split(',').map(p => parseInt(p.trim()));
const result = await apiRequest(`/images/${args.imageSpawn}/spawn`, "POST", payload, keys);
console.log(`${GREEN}Service spawned from image${RESET}`);
console.log(`Service ID: ${result.id || 'N/A'}`);
return;
}
if (args.imageClone) {
const payload: any = {};
if (args.name) payload.name = args.name;
const result = await apiRequest(`/images/${args.imageClone}/clone`, "POST", payload, keys);
console.log(`${GREEN}Image cloned successfully${RESET}`);
console.log(`Image ID: ${result.id || 'N/A'}`);
return;
}
console.error(`${RED}Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${RESET}`);
process.exit(1);
}
async function cmdKey(args: Args): Promise<void> {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
await validateKey(keys, args.extend);
}
function parseArgs(argv: string[]): Args {
const args: Args = {
command: null,
sourceFile: null,
env: [],
files: [],
artifacts: false,
outputDir: null,
network: null,
vcpu: null,
apiKey: null,
publicKey: null,
account: null,
shell: null,
list: false,
attach: null,
kill: null,
audit: false,
tmux: false,
screen: false,
name: null,
ports: null,
domains: null,
type: null,
bootstrap: null,
bootstrapFile: null,
info: null,
logs: null,
tail: null,
sleep: null,
wake: null,
destroy: null,
resize: null,
execute: null,
command_arg: null,
dumpBootstrap: null,
dumpFile: null,
extend: false,
envFile: null,
envAction: null,
envTarget: null,
jsonOutput: false,
imageInfo: null,
imageDelete: null,
imageLock: null,
imageUnlock: null,
imagePublish: null,
sourceType: null,
imageVisibility: null,
visibilityMode: null,
imageSpawn: null,
imageClone: null,
unfreezeOnDemand: null,
setUnfreezeOnDemand: null,
showFreezePage: null,
setShowFreezePage: null,
redeploy: null,
};
let i = 2;
while (i < argv.length) {
const arg = argv[i];
if (arg === 'session' || arg === 'service' || arg === 'key' || arg === 'languages' || arg === 'image') {
args.command = arg;
i++;
} else if (arg === '--json') {
args.jsonOutput = true;
i++;
} else if (arg === '-e' && i + 1 < argv.length) {
args.env.push(argv[++i]);
i++;
} else if (arg === '-f' && i + 1 < argv.length) {
args.files.push(argv[++i]);
i++;
} else if (arg === '-a') {
args.artifacts = true;
i++;
} else if (arg === '-o' && i + 1 < argv.length) {
args.outputDir = argv[++i];
i++;
} else if (arg === '-n' && i + 1 < argv.length) {
args.network = argv[++i];
i++;
} else if (arg === '-v' && i + 1 < argv.length) {
args.vcpu = parseInt(argv[++i]);
i++;
} else if (arg === '-k' && i + 1 < argv.length) {
args.apiKey = argv[++i];
i++;
} else if (arg === '-p' && i + 1 < argv.length) {
args.publicKey = argv[++i];
i++;
} else if (arg === '--account' && i + 1 < argv.length) {
args.account = parseInt(argv[++i]);
i++;
} else if (arg === '-s' || arg === '--shell') {
args.shell = argv[++i];
i++;
} else if (arg === '-l' || arg === '--list') {
args.list = true;
i++;
} else if (arg === '--attach' && i + 1 < argv.length) {
args.attach = argv[++i];
i++;
} else if (arg === '--kill' && i + 1 < argv.length) {
args.kill = argv[++i];
i++;
} else if (arg === '--audit') {
args.audit = true;
i++;
} else if (arg === '--tmux') {
args.tmux = true;
i++;
} else if (arg === '--screen') {
args.screen = true;
i++;
} else if (arg === '--name' && i + 1 < argv.length) {
args.name = argv[++i];
i++;
} else if (arg === '--ports' && i + 1 < argv.length) {
args.ports = argv[++i];
i++;
} else if (arg === '--domains' && i + 1 < argv.length) {
args.domains = argv[++i];
i++;
} else if (arg === '--type' && i + 1 < argv.length) {
args.type = argv[++i];
i++;
} else if (arg === '--bootstrap' && i + 1 < argv.length) {
args.bootstrap = argv[++i];
i++;
} else if (arg === '--bootstrap-file' && i + 1 < argv.length) {
args.bootstrapFile = argv[++i];
i++;
} else if (arg === '--env-file' && i + 1 < argv.length) {
args.envFile = argv[++i];
i++;
} else if (arg === 'env') {
// Handle "service env <action> <target>" subcommand
if (args.command === 'service') {
if (i + 1 < argv.length) {
args.envAction = argv[++i];
}
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
args.envTarget = argv[++i];
}
}
i++;
} else if (arg === '--info' && i + 1 < argv.length) {
if (args.command === 'image') {
args.imageInfo = argv[++i];
} else {
args.info = argv[++i];
}
i++;
} else if (arg === '--delete' && i + 1 < argv.length) {
if (args.command === 'image') {
args.imageDelete = argv[++i];
}
i++;
} else if (arg === '--lock' && i + 1 < argv.length) {
args.imageLock = argv[++i];
i++;
} else if (arg === '--unlock' && i + 1 < argv.length) {
args.imageUnlock = argv[++i];
i++;
} else if (arg === '--publish' && i + 1 < argv.length) {
args.imagePublish = argv[++i];
i++;
} else if (arg === '--source-type' && i + 1 < argv.length) {
args.sourceType = argv[++i];
i++;
} else if (arg === '--visibility' && i + 1 < argv.length) {
args.imageVisibility = argv[++i];
i++;
if (i < argv.length && !argv[i].startsWith('-')) {
args.visibilityMode = argv[i];
i++;
}
} else if (arg === '--spawn' && i + 1 < argv.length) {
args.imageSpawn = argv[++i];
i++;
} else if (arg === '--logs' && i + 1 < argv.length) {
args.logs = argv[++i];
i++;
} else if (arg === '--tail' && i + 1 < argv.length) {
args.tail = argv[++i];
i++;
} else if (arg === '--freeze' && i + 1 < argv.length) {
args.sleep = argv[++i];
i++;
} else if (arg === '--unfreeze' && i + 1 < argv.length) {
args.wake = argv[++i];
i++;
} else if (arg === '--destroy' && i + 1 < argv.length) {
args.destroy = argv[++i];
i++;
} else if (arg === '--resize' && i + 1 < argv.length) {
args.resize = argv[++i];
i++;
} else if (arg === '--execute' && i + 1 < argv.length) {
args.execute = argv[++i];
i++;
} else if (arg === '--command' && i + 1 < argv.length) {
args.command_arg = argv[++i];
i++;
} else if (arg === '--dump-bootstrap' && i + 1 < argv.length) {
args.dumpBootstrap = argv[++i];
i++;
} else if (arg === '--dump-file' && i + 1 < argv.length) {
args.dumpFile = argv[++i];
i++;
} else if (arg === '--extend') {
args.extend = true;
i++;
} else if (arg === '--clone' && i + 1 < argv.length) {
if (args.command === 'image') {
args.imageClone = argv[++i];
} else {
args.clone = argv[++i];
}
i++;
} else if (arg === '--unfreeze-on-demand') {
args.unfreezeOnDemand = true;
i++;
} else if (arg === '--set-unfreeze-on-demand' && i + 1 < argv.length) {
args.setUnfreezeOnDemand = argv[++i];
i++;
} else if (arg === '--show-freeze-page') {
args.showFreezePage = true;
i++;
} else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) {
args.setShowFreezePage = argv[++i];
i++;
} else if (arg === '--redeploy' && i + 1 < argv.length) {
args.redeploy = argv[++i];
i++;
} else if (!arg.startsWith('-')) {
args.sourceFile = arg;
i++;
} else {
console.error(`${RED}Unknown option: ${arg}${RESET}`);
process.exit(1);
}
}
return args;
}
async function main(): Promise<void> {
const args = parseArgs(process.argv);
if (args.command === 'session') {
await cmdSession(args);
} else if (args.command === 'service') {
// Check for "service env" subcommand
if (args.envAction) {
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
await cmdServiceEnv(args.envAction, args.envTarget!, args.env, args.envFile, keys);
} else {
await cmdService(args);
}
} else if (args.command === 'image') {
await cmdImage(args);
} else if (args.command === 'key') {
await cmdKey(args);
} else if (args.command === 'languages') {
await cmdLanguages(args);
} else if (args.sourceFile) {
await cmdExecute(args);
} else {
console.log(`Unsandbox CLI - Execute code in secure sandboxes
Usage:
${process.argv[1]} [options] <source_file>
${process.argv[1]} session [options]
${process.argv[1]} service [options]
${process.argv[1]} image [options]
${process.argv[1]} key [options]
${process.argv[1]} languages [--json]
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)
-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:
-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 or URI
--bootstrap-file FILE Upload local file as bootstrap script
-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
--resize ID Resize service (requires -v)
--redeploy ID Re-run bootstrap (with optional --bootstrap, --bootstrap-file, -f)
--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)
--unfreeze-on-demand Enable unfreeze on demand (with --name or --set-unfreeze-on-demand)
--set-unfreeze-on-demand ID Set unfreeze on demand for service
--show-freeze-page Enable show freeze page (with --name or --set-show-freeze-page)
--set-show-freeze-page ID Set show freeze page for service
Image options:
--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
--source-type TYPE Source type: service or snapshot
--visibility ID MODE Set visibility: private, unlisted, 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
Key options:
--extend Open browser to extend key expiration
Languages options:
--json Output as JSON array
`);
process.exit(1);
}
}
main().catch(err => {
console.error(`${RED}${err}${RESET}`);
process.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