/* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY * * unsandbox.com Java SDK (Synchronous) * * Library Usage: * import Un; * import java.util.Map; * import java.util.List; * * // Execute code synchronously * Map result = Un.executeCode("python", "print('hello')", publicKey, secretKey); * * // Execute asynchronously * String jobId = Un.executeAsync("javascript", "console.log('hello')", publicKey, secretKey); * * // Wait for job completion with exponential backoff * Map result = Un.waitForJob(jobId, publicKey, secretKey, 60000); * * // List all jobs * List> jobs = Un.listJobs(publicKey, secretKey); * * // Get supported languages * List languages = Un.getLanguages(publicKey, secretKey); * * // Snapshot operations * String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false); * * Authentication Priority (4-tier): * 1. Method arguments (publicKey, secretKey) * 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) * 3. Config file (~/.unsandbox/accounts.csv, line 0 by default) * 4. Local directory (./accounts.csv, line 0 by default) * * Request Authentication (HMAC-SHA256): * Authorization: Bearer * X-Timestamp: * X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body") * * Languages Cache: * - Cached in ~/.unsandbox/languages.json * - TTL: 1 hour * - Updated on successful API calls */ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.*; /** * Un SDK - Synchronous Java client for the unsandbox.com API. * *

This class provides methods to execute code in secure sandboxed environments, * manage jobs, and work with snapshots. * *

Example usage: *

{@code
 * Map result = Un.executeCode("python", "print('hello')", null, null);
 * System.out.println(result.get("stdout"));
 * }
* * @see unsandbox.com */ public class Un { private static final String API_BASE = "https://api.unsandbox.com"; private static final int[] POLL_DELAYS_MS = {300, 450, 700, 900, 650, 1600, 2000}; private static final long LANGUAGES_CACHE_TTL_MS = 3600 * 1000; // 1 hour private static final int DEFAULT_TIMEOUT_MS = 120000; // 2 minutes /** * Exception thrown when credentials cannot be found or are invalid. */ public static class CredentialsException extends RuntimeException { public CredentialsException(String message) { super(message); } } /** * Exception thrown when an API request fails. */ public static class ApiException extends RuntimeException { private final int statusCode; private final String responseBody; public ApiException(String message, int statusCode, String responseBody) { super(message); this.statusCode = statusCode; this.responseBody = responseBody; } public int getStatusCode() { return statusCode; } public String getResponseBody() { return responseBody; } } // ======================================================================== // Credential Resolution // ======================================================================== private static Path getUnsandboxDir() { String home = System.getProperty("user.home"); Path unsandboxDir = Paths.get(home, ".unsandbox"); try { if (!Files.exists(unsandboxDir)) { Files.createDirectories(unsandboxDir); } } catch (IOException e) { // Ignore - will fail later if needed } return unsandboxDir; } private static String[] loadCredentialsFromCsv(Path csvPath, int accountIndex) { if (!Files.exists(csvPath)) { return null; } try (BufferedReader reader = Files.newBufferedReader(csvPath)) { String line; int lineIndex = 0; while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) { continue; } if (lineIndex == accountIndex) { String[] parts = line.split(","); if (parts.length >= 2) { return new String[]{parts[0].trim(), parts[1].trim()}; } } lineIndex++; } } catch (IOException e) { // Ignore - will try next source } return null; } private static String[] resolveCredentials(String publicKey, String secretKey) { // Tier 1: Method arguments if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) { return new String[]{publicKey, secretKey}; } // Tier 2: Environment variables String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY"); String envSk = System.getenv("UNSANDBOX_SECRET_KEY"); if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) { return new String[]{envPk, envSk}; } // Determine account index int accountIndex = 0; String accountEnv = System.getenv("UNSANDBOX_ACCOUNT"); if (accountEnv != null && !accountEnv.isEmpty()) { try { accountIndex = Integer.parseInt(accountEnv); } catch (NumberFormatException e) { // Use default } } // Tier 3: ~/.unsandbox/accounts.csv Path unsandboxDir = getUnsandboxDir(); String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex); if (creds != null) { return creds; } // Tier 4: ./accounts.csv creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex); if (creds != null) { return creds; } throw new CredentialsException( "No credentials found. Please provide via:\n" + " 1. Method arguments (publicKey, secretKey)\n" + " 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" + " 3. ~/.unsandbox/accounts.csv\n" + " 4. ./accounts.csv" ); } // ======================================================================== // HMAC-SHA256 Signing // ======================================================================== private static String signRequest(String secretKey, long timestamp, String method, String path, String body) { try { String bodyStr = (body != null) ? body : ""; String message = timestamp + ":" + method + ":" + path + ":" + bodyStr; Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec( secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256" ); mac.init(secretKeySpec); byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); // Convert to lowercase hex StringBuilder hexString = new StringBuilder(); for (byte b : hash) { hexString.append(String.format("%02x", b)); } return hexString.toString(); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("Failed to compute HMAC-SHA256", e); } } // ======================================================================== // HTTP Request // ======================================================================== private static Map makeRequest( String method, String path, String publicKey, String secretKey, Map data ) throws IOException { String url = API_BASE + path; long timestamp = System.currentTimeMillis() / 1000; String body = (data != null) ? mapToJson(data) : ""; String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null); HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(method); conn.setConnectTimeout(DEFAULT_TIMEOUT_MS); conn.setReadTimeout(DEFAULT_TIMEOUT_MS); conn.setRequestProperty("Authorization", "Bearer " + publicKey); conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp)); conn.setRequestProperty("X-Signature", signature); conn.setRequestProperty("Content-Type", "application/json"); if ("POST".equals(method) && data != null) { conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } } int responseCode = conn.getResponseCode(); String responseBody; InputStream inputStream = (responseCode >= 200 && responseCode < 300) ? conn.getInputStream() : conn.getErrorStream(); if (inputStream == null) { responseBody = ""; } else { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } responseBody = sb.toString(); } } if (responseCode < 200 || responseCode >= 300) { throw new ApiException( "API request failed with status " + responseCode, responseCode, responseBody ); } return parseJson(responseBody); } // ======================================================================== // Simple JSON Serialization/Deserialization // ======================================================================== @SuppressWarnings("unchecked") private static Map parseJson(String json) { if (json == null || json.trim().isEmpty()) { return new HashMap<>(); } json = json.trim(); if (!json.startsWith("{")) { throw new RuntimeException("Invalid JSON: expected object"); } return (Map) parseValue(json, new int[]{0}); } private static Object parseValue(String json, int[] pos) { skipWhitespace(json, pos); char c = json.charAt(pos[0]); if (c == '{') { return parseObject(json, pos); } else if (c == '[') { return parseArray(json, pos); } else if (c == '"') { return parseString(json, pos); } else if (c == 't' || c == 'f') { return parseBoolean(json, pos); } else if (c == 'n') { return parseNull(json, pos); } else if (c == '-' || Character.isDigit(c)) { return parseNumber(json, pos); } throw new RuntimeException("Unexpected character at position " + pos[0]); } private static void skipWhitespace(String json, int[] pos) { while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) { pos[0]++; } } private static Map parseObject(String json, int[] pos) { Map result = new LinkedHashMap<>(); pos[0]++; // skip '{' skipWhitespace(json, pos); if (json.charAt(pos[0]) == '}') { pos[0]++; return result; } while (true) { skipWhitespace(json, pos); String key = parseString(json, pos); skipWhitespace(json, pos); if (json.charAt(pos[0]) != ':') { throw new RuntimeException("Expected ':' at position " + pos[0]); } pos[0]++; Object value = parseValue(json, pos); result.put(key, value); skipWhitespace(json, pos); char c = json.charAt(pos[0]); if (c == '}') { pos[0]++; break; } else if (c == ',') { pos[0]++; } else { throw new RuntimeException("Expected ',' or '}' at position " + pos[0]); } } return result; } private static List parseArray(String json, int[] pos) { List result = new ArrayList<>(); pos[0]++; // skip '[' skipWhitespace(json, pos); if (json.charAt(pos[0]) == ']') { pos[0]++; return result; } while (true) { result.add(parseValue(json, pos)); skipWhitespace(json, pos); char c = json.charAt(pos[0]); if (c == ']') { pos[0]++; break; } else if (c == ',') { pos[0]++; } else { throw new RuntimeException("Expected ',' or ']' at position " + pos[0]); } } return result; } private static String parseString(String json, int[] pos) { pos[0]++; // skip opening quote StringBuilder sb = new StringBuilder(); while (pos[0] < json.length()) { char c = json.charAt(pos[0]); if (c == '"') { pos[0]++; return sb.toString(); } else if (c == '\\') { pos[0]++; if (pos[0] < json.length()) { char escaped = json.charAt(pos[0]); switch (escaped) { case '"': sb.append('"'); break; case '\\': sb.append('\\'); break; case '/': sb.append('/'); break; case 'b': sb.append('\b'); break; case 'f': sb.append('\f'); break; case 'n': sb.append('\n'); break; case 'r': sb.append('\r'); break; case 't': sb.append('\t'); break; case 'u': String hex = json.substring(pos[0] + 1, pos[0] + 5); sb.append((char) Integer.parseInt(hex, 16)); pos[0] += 4; break; default: sb.append(escaped); } } } else { sb.append(c); } pos[0]++; } throw new RuntimeException("Unterminated string"); } private static Object parseNumber(String json, int[] pos) { int start = pos[0]; boolean isDouble = false; if (json.charAt(pos[0]) == '-') pos[0]++; while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++; if (pos[0] < json.length() && json.charAt(pos[0]) == '.') { isDouble = true; pos[0]++; while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++; } if (pos[0] < json.length() && (json.charAt(pos[0]) == 'e' || json.charAt(pos[0]) == 'E')) { isDouble = true; pos[0]++; if (pos[0] < json.length() && (json.charAt(pos[0]) == '+' || json.charAt(pos[0]) == '-')) pos[0]++; while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++; } String numStr = json.substring(start, pos[0]); if (isDouble) { return Double.parseDouble(numStr); } else { long value = Long.parseLong(numStr); if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { return (int) value; } return value; } } private static Boolean parseBoolean(String json, int[] pos) { if (json.startsWith("true", pos[0])) { pos[0] += 4; return true; } else if (json.startsWith("false", pos[0])) { pos[0] += 5; return false; } throw new RuntimeException("Invalid boolean at position " + pos[0]); } private static Object parseNull(String json, int[] pos) { if (json.startsWith("null", pos[0])) { pos[0] += 4; return null; } throw new RuntimeException("Invalid null at position " + pos[0]); } private static String mapToJson(Map map) { StringBuilder sb = new StringBuilder(); sb.append("{"); boolean first = true; for (Map.Entry entry : map.entrySet()) { if (!first) sb.append(","); first = false; sb.append("\"").append(escapeJsonString(entry.getKey())).append("\":"); sb.append(valueToJson(entry.getValue())); } sb.append("}"); return sb.toString(); } private static String valueToJson(Object value) { if (value == null) { return "null"; } else if (value instanceof String) { return "\"" + escapeJsonString((String) value) + "\""; } else if (value instanceof Number) { return value.toString(); } else if (value instanceof Boolean) { return value.toString(); } else if (value instanceof Map) { @SuppressWarnings("unchecked") Map map = (Map) value; return mapToJson(map); } else if (value instanceof List) { StringBuilder sb = new StringBuilder(); sb.append("["); boolean first = true; for (Object item : (List) value) { if (!first) sb.append(","); first = false; sb.append(valueToJson(item)); } sb.append("]"); return sb.toString(); } return "\"" + escapeJsonString(value.toString()) + "\""; } private static String escapeJsonString(String s) { StringBuilder sb = new StringBuilder(); for (char c : s.toCharArray()) { switch (c) { case '"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; case '\b': sb.append("\\b"); break; case '\f': sb.append("\\f"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; default: if (c < 0x20) { sb.append(String.format("\\u%04x", (int) c)); } else { sb.append(c); } } } return sb.toString(); } // ======================================================================== // Languages Cache // ======================================================================== private static Path getLanguagesCachePath() { return getUnsandboxDir().resolve("languages.json"); } @SuppressWarnings("unchecked") private static List loadLanguagesCache() { Path cachePath = getLanguagesCachePath(); if (!Files.exists(cachePath)) { return null; } try { long mtime = Files.getLastModifiedTime(cachePath).toMillis(); long ageMs = System.currentTimeMillis() - mtime; if (ageMs >= LANGUAGES_CACHE_TTL_MS) { return null; } String content = new String(Files.readAllBytes(cachePath), StandardCharsets.UTF_8); Map data = parseJson(content); Object languages = data.get("languages"); if (languages instanceof List) { List result = new ArrayList<>(); for (Object item : (List) languages) { if (item instanceof String) { result.add((String) item); } } return result; } } catch (IOException e) { // Cache failure is non-fatal } return null; } private static void saveLanguagesCache(List languages) { try { Path cachePath = getLanguagesCachePath(); Map data = new LinkedHashMap<>(); data.put("languages", languages); data.put("timestamp", System.currentTimeMillis() / 1000); Files.write(cachePath, mapToJson(data).getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { // Cache failure is non-fatal } } // ======================================================================== // Language Detection // ======================================================================== private static final Map LANGUAGE_MAP = new HashMap<>(); static { LANGUAGE_MAP.put("py", "python"); LANGUAGE_MAP.put("js", "javascript"); LANGUAGE_MAP.put("ts", "typescript"); LANGUAGE_MAP.put("rb", "ruby"); LANGUAGE_MAP.put("php", "php"); LANGUAGE_MAP.put("pl", "perl"); LANGUAGE_MAP.put("sh", "bash"); LANGUAGE_MAP.put("r", "r"); LANGUAGE_MAP.put("lua", "lua"); LANGUAGE_MAP.put("go", "go"); LANGUAGE_MAP.put("rs", "rust"); LANGUAGE_MAP.put("c", "c"); LANGUAGE_MAP.put("cpp", "cpp"); LANGUAGE_MAP.put("cc", "cpp"); LANGUAGE_MAP.put("cxx", "cpp"); LANGUAGE_MAP.put("java", "java"); LANGUAGE_MAP.put("kt", "kotlin"); LANGUAGE_MAP.put("m", "objc"); LANGUAGE_MAP.put("cs", "csharp"); LANGUAGE_MAP.put("fs", "fsharp"); LANGUAGE_MAP.put("hs", "haskell"); LANGUAGE_MAP.put("ml", "ocaml"); LANGUAGE_MAP.put("clj", "clojure"); LANGUAGE_MAP.put("scm", "scheme"); LANGUAGE_MAP.put("ss", "scheme"); LANGUAGE_MAP.put("erl", "erlang"); LANGUAGE_MAP.put("ex", "elixir"); LANGUAGE_MAP.put("exs", "elixir"); LANGUAGE_MAP.put("jl", "julia"); LANGUAGE_MAP.put("d", "d"); LANGUAGE_MAP.put("nim", "nim"); LANGUAGE_MAP.put("zig", "zig"); LANGUAGE_MAP.put("v", "v"); LANGUAGE_MAP.put("cr", "crystal"); LANGUAGE_MAP.put("dart", "dart"); LANGUAGE_MAP.put("groovy", "groovy"); LANGUAGE_MAP.put("f90", "fortran"); LANGUAGE_MAP.put("f95", "fortran"); LANGUAGE_MAP.put("lisp", "commonlisp"); LANGUAGE_MAP.put("lsp", "commonlisp"); LANGUAGE_MAP.put("cob", "cobol"); LANGUAGE_MAP.put("tcl", "tcl"); LANGUAGE_MAP.put("raku", "raku"); LANGUAGE_MAP.put("pro", "prolog"); LANGUAGE_MAP.put("p", "prolog"); LANGUAGE_MAP.put("4th", "forth"); LANGUAGE_MAP.put("forth", "forth"); LANGUAGE_MAP.put("fth", "forth"); } /** * Detect programming language from filename extension. * * @param filename Filename to detect language from (e.g., "script.py") * @return Language identifier (e.g., "python") or null if unknown */ public static String detectLanguage(String filename) { if (filename == null || !filename.contains(".")) { return null; } String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); return LANGUAGE_MAP.get(ext); } // ======================================================================== // Public API Methods // ======================================================================== /** * Execute code synchronously (blocks until completion). * * @param language Programming language (e.g., "python", "javascript", "go") * @param code Source code to execute * @param publicKey Optional API key (uses credentials resolution if null) * @param secretKey Optional API secret (uses credentials resolution if null) * @return Response map containing stdout, stderr, exit code, etc. * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map executeCode( String language, String code, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); publicKey = creds[0]; secretKey = creds[1]; Map data = new LinkedHashMap<>(); data.put("language", language); data.put("code", code); Map response = makeRequest("POST", "/execute", publicKey, secretKey, data); // If we got a job_id, poll until completion Object jobIdObj = response.get("job_id"); Object statusObj = response.get("status"); if (jobIdObj != null && statusObj != null) { String status = statusObj.toString(); if ("pending".equals(status) || "running".equals(status)) { return waitForJob(jobIdObj.toString(), publicKey, secretKey, 0); } } return response; } /** * Execute code asynchronously (returns immediately with job ID). * * @param language Programming language (e.g., "python", "javascript") * @param code Source code to execute * @param publicKey Optional API key * @param secretKey Optional API secret * @return Job ID string * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static String executeAsync( String language, String code, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); publicKey = creds[0]; secretKey = creds[1]; Map data = new LinkedHashMap<>(); data.put("language", language); data.put("code", code); Map response = makeRequest("POST", "/execute", publicKey, secretKey, data); Object jobId = response.get("job_id"); return jobId != null ? jobId.toString() : null; } /** * Get current status/result of a job (single poll, no waiting). * * @param jobId Job ID from executeAsync() * @param publicKey Optional API key * @param secretKey Optional API secret * @return Job response map * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map getJob( String jobId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("GET", "/jobs/" + jobId, creds[0], creds[1], null); } /** * Wait for job completion with exponential backoff polling. * *

Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...] * * @param jobId Job ID from executeAsync() * @param publicKey Optional API key * @param secretKey Optional API secret * @param timeoutMs Maximum time to wait (0 for no timeout) * @return Final job result when status is terminal (completed, failed, timeout, cancelled) * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error * @throws RuntimeException if timeout exceeded */ public static Map waitForJob( String jobId, String publicKey, String secretKey, long timeoutMs ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); publicKey = creds[0]; secretKey = creds[1]; long startTime = System.currentTimeMillis(); int pollCount = 0; while (true) { // Sleep before polling int delayIdx = Math.min(pollCount, POLL_DELAYS_MS.length - 1); try { Thread.sleep(POLL_DELAYS_MS[delayIdx]); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Wait interrupted", e); } pollCount++; // Check timeout if (timeoutMs > 0 && System.currentTimeMillis() - startTime > timeoutMs) { throw new RuntimeException("Timeout waiting for job " + jobId); } Map response = getJob(jobId, publicKey, secretKey); Object statusObj = response.get("status"); if (statusObj != null) { String status = statusObj.toString(); if ("completed".equals(status) || "failed".equals(status) || "timeout".equals(status) || "cancelled".equals(status)) { return response; } } // Still running, continue polling } } /** * Cancel a running job. * * @param jobId Job ID to cancel * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with cancellation confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map cancelJob( String jobId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("DELETE", "/jobs/" + jobId, creds[0], creds[1], null); } /** * List all jobs for the authenticated account. * * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of job maps * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ @SuppressWarnings("unchecked") public static List> listJobs( String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map response = makeRequest("GET", "/jobs", creds[0], creds[1], null); Object jobs = response.get("jobs"); if (jobs instanceof List) { List> result = new ArrayList<>(); for (Object item : (List) jobs) { if (item instanceof Map) { result.add((Map) item); } } return result; } return new ArrayList<>(); } /** * Get list of supported programming languages. * *

Results are cached for 1 hour in ~/.unsandbox/languages.json * * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of language identifiers (e.g., ["python", "javascript", "go", ...]) * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static List getLanguages( String publicKey, String secretKey ) throws IOException { // Try cache first List cached = loadLanguagesCache(); if (cached != null) { return cached; } String[] creds = resolveCredentials(publicKey, secretKey); Map response = makeRequest("GET", "/languages", creds[0], creds[1], null); Object languages = response.get("languages"); List result = new ArrayList<>(); if (languages instanceof List) { for (Object item : (List) languages) { if (item instanceof String) { result.add((String) item); } } } // Cache the result saveLanguagesCache(result); return result; } /** * Create a snapshot of a session. * * @param sessionId Session ID to snapshot * @param publicKey Optional API key * @param secretKey Optional API secret * @param name Optional snapshot name * @param ephemeral If true, snapshot is ephemeral (hot snapshot) * @return Snapshot ID * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static String sessionSnapshot( String sessionId, String publicKey, String secretKey, String name, boolean ephemeral ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("session_id", sessionId); data.put("hot", ephemeral); if (name != null && !name.isEmpty()) { data.put("name", name); } Map response = makeRequest("POST", "/snapshots", creds[0], creds[1], data); Object snapshotId = response.get("snapshot_id"); return snapshotId != null ? snapshotId.toString() : null; } /** * Create a snapshot of a service. * * @param serviceId Service ID to snapshot * @param publicKey Optional API key * @param secretKey Optional API secret * @param name Optional snapshot name * @return Snapshot ID * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static String serviceSnapshot( String serviceId, String publicKey, String secretKey, String name ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("service_id", serviceId); data.put("hot", false); if (name != null && !name.isEmpty()) { data.put("name", name); } Map response = makeRequest("POST", "/snapshots", creds[0], creds[1], data); Object snapshotId = response.get("snapshot_id"); return snapshotId != null ? snapshotId.toString() : null; } /** * List all snapshots. * * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of snapshot maps * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ @SuppressWarnings("unchecked") public static List> listSnapshots( String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map response = makeRequest("GET", "/snapshots", creds[0], creds[1], null); Object snapshots = response.get("snapshots"); if (snapshots instanceof List) { List> result = new ArrayList<>(); for (Object item : (List) snapshots) { if (item instanceof Map) { result.add((Map) item); } } return result; } return new ArrayList<>(); } /** * Restore a snapshot. * * @param snapshotId Snapshot ID to restore * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with restored resource info * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map restoreSnapshot( String snapshotId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/snapshots/" + snapshotId + "/restore", creds[0], creds[1], new LinkedHashMap<>()); } /** * Delete a snapshot. * * @param snapshotId Snapshot ID to delete * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with deletion confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map deleteSnapshot( String snapshotId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("DELETE", "/snapshots/" + snapshotId, creds[0], creds[1], null); } // ======================================================================== // Session API Methods // ======================================================================== /** * List all active sessions for the authenticated account. * * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of session maps containing id, container_name, shell, status, remaining_ttl * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ @SuppressWarnings("unchecked") public static List> listSessions( String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map response = makeRequest("GET", "/sessions", creds[0], creds[1], null); Object sessions = response.get("sessions"); if (sessions instanceof List) { List> result = new ArrayList<>(); for (Object item : (List) sessions) { if (item instanceof Map) { result.add((Map) item); } } return result; } return new ArrayList<>(); } /** * Get details of a specific session. * * @param sessionId Session ID to retrieve * @param publicKey Optional API key * @param secretKey Optional API secret * @return Session details map * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map getSession( String sessionId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("GET", "/sessions/" + sessionId, creds[0], creds[1], null); } /** * Create a new interactive session. * * @param language Programming language/shell for the session (e.g., "bash", "python3") * @param publicKey Optional API key * @param secretKey Optional API secret * @param opts Optional parameters: network_mode, ttl, shell, multiplexer, vcpu * @return Response map containing session_id, container_name * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map createSession( String language, String publicKey, String secretKey, Map opts ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("network_mode", "zerotrust"); data.put("ttl", 3600); if (language != null && !language.isEmpty()) { data.put("shell", language); } if (opts != null) { data.putAll(opts); } return makeRequest("POST", "/sessions", creds[0], creds[1], data); } /** * Delete (terminate) a session. * * @param sessionId Session ID to terminate * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with termination confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map deleteSession( String sessionId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("DELETE", "/sessions/" + sessionId, creds[0], creds[1], null); } /** * Freeze a session (pause execution, reduce resource consumption). * * @param sessionId Session ID to freeze * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with freeze confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map freezeSession( String sessionId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/sessions/" + sessionId + "/freeze", creds[0], creds[1], new LinkedHashMap<>()); } /** * Unfreeze a session (resume execution). * * @param sessionId Session ID to unfreeze * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with unfreeze confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map unfreezeSession( String sessionId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/sessions/" + sessionId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>()); } /** * Boost a session's resources (increase vCPU and memory). * * @param sessionId Session ID to boost * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with boost confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map boostSession( String sessionId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("vcpu", 2); return makeRequest("POST", "/sessions/" + sessionId + "/boost", creds[0], creds[1], data); } /** * Remove boost from a session (return to base resources). * * @param sessionId Session ID to unboost * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with unboost confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map unboostSession( String sessionId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/sessions/" + sessionId + "/unboost", creds[0], creds[1], new LinkedHashMap<>()); } /** * Execute a shell command in an existing session. * * @param sessionId Session ID to execute command in * @param command Shell command to execute * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with command output * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map shellSession( String sessionId, String command, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("command", command); return makeRequest("POST", "/sessions/" + sessionId + "/shell", creds[0], creds[1], data); } // ======================================================================== // Service API Methods // ======================================================================== /** * List all services for the authenticated account. * * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of service maps containing id, name, state, ports, disk_used * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ @SuppressWarnings("unchecked") public static List> listServices( String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map response = makeRequest("GET", "/services", creds[0], creds[1], null); Object services = response.get("services"); if (services instanceof List) { List> result = new ArrayList<>(); for (Object item : (List) services) { if (item instanceof Map) { result.add((Map) item); } } return result; } return new ArrayList<>(); } /** * Create a new persistent service. * * @param name Service name * @param ports Comma-separated list of ports to expose (e.g., "80,443") * @param bootstrap Bootstrap script or URL to run on service creation * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing service_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map createService( String name, String ports, String bootstrap, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("name", name); if (ports != null && !ports.isEmpty()) { // Parse ports string into list List portList = new ArrayList<>(); for (String p : ports.split(",")) { try { portList.add(Integer.parseInt(p.trim())); } catch (NumberFormatException e) { // Skip invalid port } } data.put("ports", portList); } if (bootstrap != null && !bootstrap.isEmpty()) { if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) { data.put("bootstrap_url", bootstrap); } else { data.put("bootstrap", bootstrap); } } return makeRequest("POST", "/services", creds[0], creds[1], data); } /** * Create a new service (long-running container) with unfreeze-on-demand option. * * @param name Service name (used for hostname) * @param ports Comma-separated list of ports to expose (e.g., "80,443") * @param bootstrap Bootstrap script or URL to run on service creation * @param unfreezeOnDemand If true, frozen service will auto-wake on HTTP request * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing service_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map createService( String name, String ports, String bootstrap, boolean unfreezeOnDemand, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("name", name); if (ports != null && !ports.isEmpty()) { // Parse ports string into list List portList = new ArrayList<>(); for (String p : ports.split(",")) { try { portList.add(Integer.parseInt(p.trim())); } catch (NumberFormatException e) { // Skip invalid port } } data.put("ports", portList); } if (bootstrap != null && !bootstrap.isEmpty()) { if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) { data.put("bootstrap_url", bootstrap); } else { data.put("bootstrap", bootstrap); } } if (unfreezeOnDemand) { data.put("unfreeze_on_demand", true); } return makeRequest("POST", "/services", creds[0], creds[1], data); } /** * Get details of a specific service. * * @param serviceId Service ID to retrieve * @param publicKey Optional API key * @param secretKey Optional API secret * @return Service details map * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map getService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("GET", "/services/" + serviceId, creds[0], creds[1], null); } /** * Update a service's configuration. * * @param serviceId Service ID to update * @param opts Update options (e.g., vcpu for resizing) * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with update confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map updateService( String serviceId, Map opts, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], opts); } /** * Delete (destroy) a service. * * @param serviceId Service ID to destroy * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with deletion confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map deleteService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("DELETE", "/services/" + serviceId, creds[0], creds[1], null); } /** * Freeze a service (pause execution, reduce resource consumption). * * @param serviceId Service ID to freeze * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with freeze confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map freezeService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/services/" + serviceId + "/freeze", creds[0], creds[1], new LinkedHashMap<>()); } /** * Unfreeze a service (resume execution). * * @param serviceId Service ID to unfreeze * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with unfreeze confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map unfreezeService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/services/" + serviceId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>()); } /** * Lock a service (prevent modifications and termination). * * @param serviceId Service ID to lock * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with lock confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map lockService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/services/" + serviceId + "/lock", creds[0], creds[1], new LinkedHashMap<>()); } /** * Unlock a service (allow modifications and termination). * * @param serviceId Service ID to unlock * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with unlock confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map unlockService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/services/" + serviceId + "/unlock", creds[0], creds[1], new LinkedHashMap<>()); } /** * Set unfreeze-on-demand for a service. * *

When enabled, a frozen service will automatically wake up when it receives * an HTTP request, without requiring an explicit unfreeze API call. * * @param serviceId Service ID to configure * @param enabled True to enable unfreeze-on-demand, false to disable * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with update confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map setUnfreezeOnDemand( String serviceId, boolean enabled, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("unfreeze_on_demand", enabled); return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data); } /** * Get bootstrap logs for a service. * * @param serviceId Service ID to get logs for * @param all If true, return all logs; if false, return recent logs only * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing logs * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map getServiceLogs( String serviceId, boolean all, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); String path = "/services/" + serviceId + "/logs"; if (all) { path += "?all=true"; } return makeRequest("GET", path, creds[0], creds[1], null); } /** * Get environment vault status for a service. * * @param serviceId Service ID to get env status for * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing has_vault, count, updated_at * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map getServiceEnv( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("GET", "/services/" + serviceId + "/env", creds[0], creds[1], null); } /** * Set environment vault for a service. * * @param serviceId Service ID to set env for * @param env Environment variables map (KEY=VALUE pairs) * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with set confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map setServiceEnv( String serviceId, Map env, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("env", env); return makeRequest("POST", "/services/" + serviceId + "/env", creds[0], creds[1], data); } /** * Delete environment variables from a service's vault. * * @param serviceId Service ID to delete env from * @param keys List of keys to delete (null to delete entire vault) * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with deletion confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map deleteServiceEnv( String serviceId, List keys, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); String path = "/services/" + serviceId + "/env"; if (keys != null && !keys.isEmpty()) { // URL encode keys parameter path += "?keys=" + String.join(",", keys); } return makeRequest("DELETE", path, creds[0], creds[1], null); } /** * Export environment vault for a service (returns decrypted values). * * @param serviceId Service ID to export env from * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing exported environment variables * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map exportServiceEnv( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/services/" + serviceId + "/env/export", creds[0], creds[1], new LinkedHashMap<>()); } /** * Redeploy a service (re-run bootstrap script). * * @param serviceId Service ID to redeploy * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with redeploy confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map redeployService( String serviceId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>()); } /** * Execute a command in a service container. * * @param serviceId Service ID to execute command in * @param command Command to execute * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing stdout, stderr, exit_code * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map executeInService( String serviceId, String command, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("command", command); return makeRequest("POST", "/services/" + serviceId + "/execute", creds[0], creds[1], data); } // ======================================================================== // Additional Snapshot API Methods // ======================================================================== /** * Lock a snapshot (prevent deletion). * * @param snapshotId Snapshot ID to lock * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with lock confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map lockSnapshot( String snapshotId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/snapshots/" + snapshotId + "/lock", creds[0], creds[1], new LinkedHashMap<>()); } /** * Unlock a snapshot (allow deletion). * * @param snapshotId Snapshot ID to unlock * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with unlock confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map unlockSnapshot( String snapshotId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/snapshots/" + snapshotId + "/unlock", creds[0], creds[1], new LinkedHashMap<>()); } /** * Clone a snapshot to create a new snapshot with a different name. * * @param snapshotId Snapshot ID to clone * @param name Name for the cloned snapshot * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing new snapshot_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map cloneSnapshot( String snapshotId, String name, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); if (name != null && !name.isEmpty()) { data.put("name", name); } return makeRequest("POST", "/snapshots/" + snapshotId + "/clone", creds[0], creds[1], data); } // ======================================================================== // Images API Methods (LXD Container Images) // ======================================================================== /** * Publish a session or service as a reusable LXD container image. * * @param sourceType Source type: "session" or "service" * @param sourceId ID of the session or service to publish * @param name Name for the image * @param description Optional description for the image * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing image_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map imagePublish( String sourceType, String sourceId, String name, String description, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("source_type", sourceType); data.put("source_id", sourceId); data.put("name", name); if (description != null && !description.isEmpty()) { data.put("description", description); } return makeRequest("POST", "/images", creds[0], creds[1], data); } /** * List container images with optional filtering. * * @param filterType Optional filter: "own", "shared", "public", or null for all * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of image maps containing id, name, description, visibility, etc. * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ @SuppressWarnings("unchecked") public static List> listImages( String filterType, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); String path = "/images"; if (filterType != null && !filterType.isEmpty()) { path = "/images/" + filterType; } Map response = makeRequest("GET", path, creds[0], creds[1], null); Object images = response.get("images"); if (images instanceof List) { List> result = new ArrayList<>(); for (Object item : (List) images) { if (item instanceof Map) { result.add((Map) item); } } return result; } return new ArrayList<>(); } /** * Get details of a specific image. * * @param imageId Image ID to retrieve * @param publicKey Optional API key * @param secretKey Optional API secret * @return Image details map * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map getImage( String imageId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("GET", "/images/" + imageId, creds[0], creds[1], null); } /** * Delete an image. * * @param imageId Image ID to delete * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with deletion confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map deleteImage( String imageId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("DELETE", "/images/" + imageId, creds[0], creds[1], null); } /** * Lock an image (prevent deletion and modifications). * * @param imageId Image ID to lock * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with lock confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map lockImage( String imageId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/images/" + imageId + "/lock", creds[0], creds[1], new LinkedHashMap<>()); } /** * Unlock an image (allow deletion and modifications). * * @param imageId Image ID to unlock * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with unlock confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map unlockImage( String imageId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); return makeRequest("POST", "/images/" + imageId + "/unlock", creds[0], creds[1], new LinkedHashMap<>()); } /** * Set the visibility of an image. * * @param imageId Image ID to modify * @param visibility Visibility level: "private", "shared", or "public" * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with visibility change confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map setImageVisibility( String imageId, String visibility, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("visibility", visibility); return makeRequest("POST", "/images/" + imageId + "/visibility", creds[0], creds[1], data); } /** * Publish an image from a service or snapshot. * * @param sourceType Source type: "service" or "snapshot" * @param sourceId Source ID (service_id or snapshot_id) * @param name Name for the new image (optional) * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing new image_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map publishImage( String sourceType, String sourceId, String name, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("source_type", sourceType); data.put("source_id", sourceId); if (name != null && !name.isEmpty()) { data.put("name", name); } return makeRequest("POST", "/images/publish", creds[0], creds[1], data); } /** * Grant access to an image for another API key (for shared images). * * @param imageId Image ID to grant access to * @param trustedApiKey Public API key to grant access to * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with grant confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map grantImageAccess( String imageId, String trustedApiKey, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("trusted_api_key", trustedApiKey); return makeRequest("POST", "/images/" + imageId + "/grant", creds[0], creds[1], data); } /** * Revoke access to an image from another API key. * * @param imageId Image ID to revoke access from * @param trustedApiKey Public API key to revoke access from * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with revoke confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map revokeImageAccess( String imageId, String trustedApiKey, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("trusted_api_key", trustedApiKey); return makeRequest("POST", "/images/" + imageId + "/revoke", creds[0], creds[1], data); } /** * List API keys that have been granted access to an image. * * @param imageId Image ID to list trusted keys for * @param publicKey Optional API key * @param secretKey Optional API secret * @return List of trusted API key maps * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ @SuppressWarnings("unchecked") public static List> listImageTrusted( String imageId, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map response = makeRequest("GET", "/images/" + imageId + "/trusted", creds[0], creds[1], null); Object trusted = response.get("trusted"); if (trusted instanceof List) { List> result = new ArrayList<>(); for (Object item : (List) trusted) { if (item instanceof Map) { result.add((Map) item); } } return result; } return new ArrayList<>(); } /** * Transfer ownership of an image to another API key. * * @param imageId Image ID to transfer * @param toApiKey Public API key of the new owner * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map with transfer confirmation * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map transferImage( String imageId, String toApiKey, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("to_api_key", toApiKey); return makeRequest("POST", "/images/" + imageId + "/transfer", creds[0], creds[1], data); } /** * Spawn a new service from an image. * * @param imageId Image ID to spawn from * @param name Name for the new service * @param ports Comma-separated list of ports to expose (e.g., "80,443") * @param bootstrap Optional bootstrap script or URL * @param networkMode Network mode: "zerotrust" or "semitrusted" * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing service_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map spawnFromImage( String imageId, String name, String ports, String bootstrap, String networkMode, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); data.put("name", name); if (ports != null && !ports.isEmpty()) { List portList = new ArrayList<>(); for (String p : ports.split(",")) { try { portList.add(Integer.parseInt(p.trim())); } catch (NumberFormatException e) { // Skip invalid port } } data.put("ports", portList); } if (bootstrap != null && !bootstrap.isEmpty()) { if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) { data.put("bootstrap_url", bootstrap); } else { data.put("bootstrap", bootstrap); } } if (networkMode != null && !networkMode.isEmpty()) { data.put("network_mode", networkMode); } return makeRequest("POST", "/images/" + imageId + "/spawn", creds[0], creds[1], data); } /** * Clone an image to create a new image with a different name. * * @param imageId Image ID to clone * @param name Name for the cloned image * @param description Optional description for the cloned image * @param publicKey Optional API key * @param secretKey Optional API secret * @return Response map containing new image_id * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map cloneImage( String imageId, String name, String description, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); Map data = new LinkedHashMap<>(); if (name != null && !name.isEmpty()) { data.put("name", name); } if (description != null && !description.isEmpty()) { data.put("description", description); } return makeRequest("POST", "/images/" + imageId + "/clone", creds[0], creds[1], data); } // ======================================================================== // Key Validation API // ======================================================================== /** * Validate API key credentials. * * @param publicKey API public key to validate * @param secretKey API secret key to validate * @return Response map with validation result (valid, tier, etc.) * @throws IOException on network errors * @throws ApiException if API returns an error (including invalid credentials) */ public static Map validateKeys( String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); // Note: validateKeys uses POST to /keys/validate return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>()); } // ======================================================================== // Image Generation API // ======================================================================== /** * Generate images from text prompt using AI. * * @param prompt Text description of the image to generate * @param model Model to use (optional, can be null) * @param size Image size (e.g., "1024x1024") * @param quality "standard" or "hd" * @param n Number of images to generate * @param publicKey API public key (optional) * @param secretKey API secret key (optional) * @return Map containing "images" (List of base64/URLs) and "created_at" * @throws IOException on network errors * @throws CredentialsException if credentials cannot be found * @throws ApiException if API returns an error */ public static Map image( String prompt, String model, String size, String quality, int n, String publicKey, String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); if (size == null) size = "1024x1024"; if (quality == null) quality = "standard"; if (n <= 0) n = 1; Map data = new LinkedHashMap<>(); data.put("prompt", prompt); data.put("size", size); data.put("quality", quality); data.put("n", n); if (model != null && !model.isEmpty()) { data.put("model", model); } return makeRequest("POST", "/image", creds[0], creds[1], data); } // ======================================================================== // HTTP Request Helpers // ======================================================================== /** * Make an HTTP request with a specified method (supports PATCH). */ private static Map makeRequestWithMethod( String method, String path, String publicKey, String secretKey, Map data ) throws IOException { String url = API_BASE + path; long timestamp = System.currentTimeMillis() / 1000; String body = (data != null) ? mapToJson(data) : ""; String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null); HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(method); conn.setConnectTimeout(DEFAULT_TIMEOUT_MS); conn.setReadTimeout(DEFAULT_TIMEOUT_MS); conn.setRequestProperty("Authorization", "Bearer " + publicKey); conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp)); conn.setRequestProperty("X-Signature", signature); conn.setRequestProperty("Content-Type", "application/json"); if (data != null) { conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } } int responseCode = conn.getResponseCode(); String responseBody; InputStream inputStream = (responseCode >= 200 && responseCode < 300) ? conn.getInputStream() : conn.getErrorStream(); if (inputStream == null) { responseBody = ""; } else { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } responseBody = sb.toString(); } } if (responseCode < 200 || responseCode >= 300) { throw new ApiException( "API request failed with status " + responseCode, responseCode, responseBody ); } return parseJson(responseBody); } // ======================================================================== // CLI Implementation // ======================================================================== /** * CLI entry point. */ public static void main(String[] args) { try { cliMain(args); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); System.exit(1); } } /** * CLI main implementation. */ public static void cliMain(String[] args) throws Exception { if (args.length == 0) { printHelp(); System.exit(0); } // Parse global options String publicKey = null; String secretKey = null; String language = null; String networkMode = "zerotrust"; int vcpu = 1; List envVars = new ArrayList<>(); List files = new ArrayList<>(); List positionalArgs = new ArrayList<>(); boolean showHelp = false; int i = 0; while (i < args.length) { String arg = args[i]; if (arg.equals("-h") || arg.equals("--help")) { showHelp = true; i++; } else if (arg.equals("-s") || arg.equals("--shell")) { if (i + 1 >= args.length) { System.err.println("Error: -s/--shell requires an argument"); System.exit(2); } language = args[++i]; i++; } else if (arg.equals("-p") || arg.equals("--public-key")) { if (i + 1 >= args.length) { System.err.println("Error: -p/--public-key requires an argument"); System.exit(2); } publicKey = args[++i]; i++; } else if (arg.equals("-k") || arg.equals("--secret-key")) { if (i + 1 >= args.length) { System.err.println("Error: -k/--secret-key requires an argument"); System.exit(2); } secretKey = args[++i]; i++; } else if (arg.equals("-n") || arg.equals("--network")) { if (i + 1 >= args.length) { System.err.println("Error: -n/--network requires an argument"); System.exit(2); } networkMode = args[++i]; i++; } else if (arg.equals("-v") || arg.equals("--vcpu")) { if (i + 1 >= args.length) { System.err.println("Error: -v/--vcpu requires an argument"); System.exit(2); } vcpu = Integer.parseInt(args[++i]); i++; } else if (arg.equals("-e") || arg.equals("--env")) { if (i + 1 >= args.length) { System.err.println("Error: -e/--env requires an argument"); System.exit(2); } envVars.add(args[++i]); i++; } else if (arg.equals("-f") || arg.equals("--file")) { if (i + 1 >= args.length) { System.err.println("Error: -f/--file requires an argument"); System.exit(2); } files.add(args[++i]); i++; } else if (arg.startsWith("-")) { System.err.println("Error: Unknown option: " + arg); System.exit(2); } else { positionalArgs.add(arg); i++; } } if (showHelp || positionalArgs.isEmpty()) { printHelp(); System.exit(0); } String command = positionalArgs.get(0); // Route to subcommand handlers switch (command) { case "session": handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language); break; case "service": handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars); break; case "snapshot": handleSnapshot(positionalArgs, publicKey, secretKey); break; case "image": handleImage(positionalArgs, publicKey, secretKey); break; case "key": handleKey(publicKey, secretKey); break; case "languages": handleLanguages(positionalArgs, publicKey, secretKey); break; default: // Default: execute code handleExecute(positionalArgs, publicKey, secretKey, language, networkMode, vcpu, envVars, files); break; } } private static void printHelp() { System.out.println("Un - unsandbox.com CLI (Java Synchronous SDK)"); System.out.println(); System.out.println("Usage:"); System.out.println(" java Un [options] Execute code file"); System.out.println(" java Un [options] -s LANG 'code' Execute inline code"); System.out.println(" java Un session [options] Interactive session"); System.out.println(" java Un service [options] Manage services"); System.out.println(" java Un snapshot [options] Manage snapshots"); System.out.println(" java Un image [options] Manage images"); System.out.println(" java Un key Check API key"); System.out.println(" java Un languages [--json] List supported languages"); System.out.println(); System.out.println("Global Options:"); System.out.println(" -s, --shell LANG Language for inline code"); System.out.println(" -e, --env KEY=VAL Set environment variable"); System.out.println(" -f, --file FILE Add input file to /tmp/"); System.out.println(" -p, --public-key KEY API public key"); System.out.println(" -k, --secret-key KEY API secret key"); System.out.println(" -n, --network MODE Network: zerotrust or semitrusted"); System.out.println(" -v, --vcpu N vCPU count (1-8)"); System.out.println(" -h, --help Show help"); System.out.println(); System.out.println("Session Options:"); System.out.println(" --list, -l List active sessions"); System.out.println(" --attach ID Reconnect to session"); System.out.println(" --kill ID Terminate session"); System.out.println(" --freeze ID Pause session"); System.out.println(" --unfreeze ID Resume session"); System.out.println(" --boost ID Add resources"); System.out.println(" --unboost ID Remove boost"); System.out.println(" --snapshot ID Create snapshot"); System.out.println(" --tmux Enable persistence with tmux"); System.out.println(" --screen Enable persistence with screen"); System.out.println(" --shell SHELL Shell/REPL to use"); System.out.println(); System.out.println("Service Options:"); System.out.println(" --list, -l List all services"); System.out.println(" --name NAME Service name (creates new)"); System.out.println(" --ports PORTS Comma-separated ports"); System.out.println(" --bootstrap CMD Bootstrap command"); System.out.println(" --info ID Get service details"); System.out.println(" --logs ID Get all logs"); System.out.println(" --freeze ID Pause service"); System.out.println(" --unfreeze ID Resume service"); System.out.println(" --destroy ID Delete service"); System.out.println(" --lock ID Prevent deletion"); System.out.println(" --unlock ID Allow deletion"); System.out.println(" --execute ID CMD Run command in service"); System.out.println(" --redeploy ID Re-run bootstrap"); System.out.println(" --snapshot ID Create snapshot"); System.out.println(); System.out.println("Service Env Subcommand:"); System.out.println(" java Un service env status ID Show vault status"); System.out.println(" java Un service env set ID Set from stdin"); System.out.println(" java Un service env export ID Export to stdout"); System.out.println(" java Un service env delete ID Delete vault"); System.out.println(); System.out.println("Snapshot Options:"); System.out.println(" --list, -l List all snapshots"); System.out.println(" --info ID Get snapshot details"); System.out.println(" --delete ID Delete snapshot"); System.out.println(" --lock ID Prevent deletion"); System.out.println(" --unlock ID Allow deletion"); System.out.println(" --clone ID Clone snapshot"); System.out.println(); System.out.println("Image Options:"); System.out.println(" --list, -l List all images"); System.out.println(" --info ID Get image details"); System.out.println(" --delete ID Delete an image"); System.out.println(" --lock ID Lock image to prevent deletion"); System.out.println(" --unlock ID Unlock image"); System.out.println(" --publish ID Publish from service/snapshot (requires --source-type)"); System.out.println(" --source-type TYPE Source type: service or snapshot"); System.out.println(" --visibility ID MODE Set visibility (private/unlisted/public)"); System.out.println(" --spawn ID Spawn service from image"); System.out.println(" --clone ID Clone an image"); System.out.println(" --name NAME Name for spawned service or cloned image"); System.out.println(" --ports PORTS Ports for spawned service"); System.out.println(); System.out.println("Languages Options:"); System.out.println(" --json Output as JSON array (for scripts)"); } private static void handleExecute( List args, String publicKey, String secretKey, String language, String networkMode, int vcpu, List envVars, List files ) throws Exception { String codeOrFile = args.get(0); String code; if (language != null) { // Inline code mode: -s python 'print(1)' code = codeOrFile; } else { // File mode: script.py Path filePath = Paths.get(codeOrFile); if (!Files.exists(filePath)) { System.err.println("Error: File not found: " + codeOrFile); System.exit(1); } code = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8); language = detectLanguage(codeOrFile); if (language == null) { System.err.println("Error: Cannot detect language from file extension: " + codeOrFile); System.exit(1); } } Map result = executeCode(language, code, publicKey, secretKey); // Print output Object stdout = result.get("stdout"); if (stdout != null && !stdout.toString().isEmpty()) { System.out.print(stdout); } Object stderr = result.get("stderr"); if (stderr != null && !stderr.toString().isEmpty()) { System.err.print(stderr); } System.out.println("---"); Object exitCode = result.get("exit_code"); System.out.println("Exit code: " + (exitCode != null ? exitCode : "0")); Object executionTime = result.get("execution_time_ms"); if (executionTime != null) { System.out.println("Execution time: " + executionTime + "ms"); } // Exit with the code's exit code if (exitCode != null && exitCode instanceof Number) { int code_exit = ((Number) exitCode).intValue(); if (code_exit != 0) { System.exit(code_exit); } } } private static void handleSession( List args, String publicKey, String secretKey, String networkMode, int vcpu, String shell ) throws Exception { // Parse session-specific options boolean list = false; String attachId = null; String killId = null; String freezeId = null; String unfreezeId = null; String boostId = null; String unboostId = null; String snapshotId = null; String snapshotName = null; boolean hot = false; boolean useTmux = false; boolean useScreen = false; int i = 1; // Skip "session" command while (i < args.size()) { String arg = args.get(i); if (arg.equals("--list") || arg.equals("-l")) { list = true; i++; } else if (arg.equals("--attach")) { if (i + 1 >= args.size()) { System.err.println("Error: --attach requires an ID"); System.exit(2); } attachId = args.get(++i); i++; } else if (arg.equals("--kill")) { if (i + 1 >= args.size()) { System.err.println("Error: --kill requires an ID"); System.exit(2); } killId = args.get(++i); i++; } else if (arg.equals("--freeze")) { if (i + 1 >= args.size()) { System.err.println("Error: --freeze requires an ID"); System.exit(2); } freezeId = args.get(++i); i++; } else if (arg.equals("--unfreeze")) { if (i + 1 >= args.size()) { System.err.println("Error: --unfreeze requires an ID"); System.exit(2); } unfreezeId = args.get(++i); i++; } else if (arg.equals("--boost")) { if (i + 1 >= args.size()) { System.err.println("Error: --boost requires an ID"); System.exit(2); } boostId = args.get(++i); i++; } else if (arg.equals("--unboost")) { if (i + 1 >= args.size()) { System.err.println("Error: --unboost requires an ID"); System.exit(2); } unboostId = args.get(++i); i++; } else if (arg.equals("--snapshot")) { if (i + 1 >= args.size()) { System.err.println("Error: --snapshot requires an ID"); System.exit(2); } snapshotId = args.get(++i); i++; } else if (arg.equals("--snapshot-name")) { if (i + 1 >= args.size()) { System.err.println("Error: --snapshot-name requires a name"); System.exit(2); } snapshotName = args.get(++i); i++; } else if (arg.equals("--hot")) { hot = true; i++; } else if (arg.equals("--tmux")) { useTmux = true; i++; } else if (arg.equals("--screen")) { useScreen = true; i++; } else if (arg.equals("--shell")) { if (i + 1 >= args.size()) { System.err.println("Error: --shell requires a value"); System.exit(2); } shell = args.get(++i); i++; } else { i++; } } if (list) { List> sessions = listSessions(publicKey, secretKey); printSessionList(sessions); } else if (attachId != null) { Map session = getSession(attachId, publicKey, secretKey); System.out.println("Session: " + attachId); printMap(session); } else if (killId != null) { deleteSession(killId, publicKey, secretKey); System.out.println("Session terminated: " + killId); } else if (freezeId != null) { freezeSession(freezeId, publicKey, secretKey); System.out.println("Session frozen: " + freezeId); } else if (unfreezeId != null) { unfreezeSession(unfreezeId, publicKey, secretKey); System.out.println("Session unfrozen: " + unfreezeId); } else if (boostId != null) { boostSession(boostId, publicKey, secretKey); System.out.println("Session boosted: " + boostId); } else if (unboostId != null) { unboostSession(unboostId, publicKey, secretKey); System.out.println("Session unboosted: " + unboostId); } else if (snapshotId != null) { String snapId = sessionSnapshot(snapshotId, publicKey, secretKey, snapshotName, hot); System.out.println("Snapshot created: " + snapId); } else { // Create new session Map opts = new LinkedHashMap<>(); opts.put("network_mode", networkMode); if (vcpu > 1) { opts.put("vcpu", vcpu); } if (useTmux) { opts.put("multiplexer", "tmux"); } else if (useScreen) { opts.put("multiplexer", "screen"); } Map result = createSession(shell != null ? shell : "bash", publicKey, secretKey, opts); System.out.println("Session created:"); printMap(result); } } private static void handleService( List args, String publicKey, String secretKey, String networkMode, int vcpu, List envVars ) throws Exception { // Check for "env" subcommand if (args.size() > 1 && args.get(1).equals("env")) { handleServiceEnv(args, publicKey, secretKey); return; } // Parse service-specific options boolean list = false; String name = null; String ports = null; String bootstrap = null; String infoId = null; String logsId = null; String freezeId = null; String unfreezeId = null; String destroyId = null; String lockId = null; String unlockId = null; String executeId = null; String executeCmd = null; String redeployId = null; String snapshotId = null; int i = 1; // Skip "service" command while (i < args.size()) { String arg = args.get(i); if (arg.equals("--list") || arg.equals("-l")) { list = true; i++; } else if (arg.equals("--name")) { if (i + 1 >= args.size()) { System.err.println("Error: --name requires a value"); System.exit(2); } name = args.get(++i); i++; } else if (arg.equals("--ports")) { if (i + 1 >= args.size()) { System.err.println("Error: --ports requires a value"); System.exit(2); } ports = args.get(++i); i++; } else if (arg.equals("--bootstrap")) { if (i + 1 >= args.size()) { System.err.println("Error: --bootstrap requires a value"); System.exit(2); } bootstrap = args.get(++i); i++; } else if (arg.equals("--info")) { if (i + 1 >= args.size()) { System.err.println("Error: --info requires an ID"); System.exit(2); } infoId = args.get(++i); i++; } else if (arg.equals("--logs")) { if (i + 1 >= args.size()) { System.err.println("Error: --logs requires an ID"); System.exit(2); } logsId = args.get(++i); i++; } else if (arg.equals("--freeze")) { if (i + 1 >= args.size()) { System.err.println("Error: --freeze requires an ID"); System.exit(2); } freezeId = args.get(++i); i++; } else if (arg.equals("--unfreeze")) { if (i + 1 >= args.size()) { System.err.println("Error: --unfreeze requires an ID"); System.exit(2); } unfreezeId = args.get(++i); i++; } else if (arg.equals("--destroy")) { if (i + 1 >= args.size()) { System.err.println("Error: --destroy requires an ID"); System.exit(2); } destroyId = args.get(++i); i++; } else if (arg.equals("--lock")) { if (i + 1 >= args.size()) { System.err.println("Error: --lock requires an ID"); System.exit(2); } lockId = args.get(++i); i++; } else if (arg.equals("--unlock")) { if (i + 1 >= args.size()) { System.err.println("Error: --unlock requires an ID"); System.exit(2); } unlockId = args.get(++i); i++; } else if (arg.equals("--execute")) { if (i + 2 >= args.size()) { System.err.println("Error: --execute requires ID and command"); System.exit(2); } executeId = args.get(++i); executeCmd = args.get(++i); i++; } else if (arg.equals("--redeploy")) { if (i + 1 >= args.size()) { System.err.println("Error: --redeploy requires an ID"); System.exit(2); } redeployId = args.get(++i); i++; } else if (arg.equals("--snapshot")) { if (i + 1 >= args.size()) { System.err.println("Error: --snapshot requires an ID"); System.exit(2); } snapshotId = args.get(++i); i++; } else { i++; } } if (list) { List> services = listServices(publicKey, secretKey); printServiceList(services); } else if (infoId != null) { Map service = getService(infoId, publicKey, secretKey); printMap(service); } else if (logsId != null) { Map logs = getServiceLogs(logsId, true, publicKey, secretKey); Object content = logs.get("logs"); if (content != null) { System.out.println(content); } } else if (freezeId != null) { freezeService(freezeId, publicKey, secretKey); System.out.println("Service frozen: " + freezeId); } else if (unfreezeId != null) { unfreezeService(unfreezeId, publicKey, secretKey); System.out.println("Service unfrozen: " + unfreezeId); } else if (destroyId != null) { deleteService(destroyId, publicKey, secretKey); System.out.println("Service destroyed: " + destroyId); } else if (lockId != null) { lockService(lockId, publicKey, secretKey); System.out.println("Service locked: " + lockId); } else if (unlockId != null) { unlockService(unlockId, publicKey, secretKey); System.out.println("Service unlocked: " + unlockId); } else if (executeId != null && executeCmd != null) { Map result = executeInService(executeId, executeCmd, publicKey, secretKey); Object stdout = result.get("stdout"); if (stdout != null) { System.out.print(stdout); } Object stderr = result.get("stderr"); if (stderr != null && !stderr.toString().isEmpty()) { System.err.print(stderr); } } else if (redeployId != null) { redeployService(redeployId, publicKey, secretKey); System.out.println("Service redeployed: " + redeployId); } else if (snapshotId != null) { String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null); System.out.println("Snapshot created: " + snapId); } else if (name != null) { // Create new service Map result = createService(name, ports, bootstrap, publicKey, secretKey); System.out.println("Service created:"); printMap(result); } else { System.err.println("Error: No service action specified. Use --list, --name, --info, etc."); System.exit(2); } } private static void handleServiceEnv( List args, String publicKey, String secretKey ) throws Exception { if (args.size() < 4) { System.err.println("Error: service env requires action and service ID"); System.err.println("Usage: java Un service env "); System.exit(2); } String action = args.get(2); String serviceId = args.get(3); switch (action) { case "status": Map status = getServiceEnv(serviceId, publicKey, secretKey); printMap(status); break; case "set": // Read env from stdin Map env = new LinkedHashMap<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) { continue; } int eqIdx = line.indexOf('='); if (eqIdx > 0) { String key = line.substring(0, eqIdx); String value = line.substring(eqIdx + 1); env.put(key, value); } } } if (!env.isEmpty()) { setServiceEnv(serviceId, env, publicKey, secretKey); System.out.println("Environment set for service: " + serviceId); } else { System.err.println("Error: No environment variables provided"); System.exit(1); } break; case "export": Map exported = exportServiceEnv(serviceId, publicKey, secretKey); Object envData = exported.get("env"); if (envData instanceof Map) { @SuppressWarnings("unchecked") Map envMap = (Map) envData; for (Map.Entry entry : envMap.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); } } break; case "delete": deleteServiceEnv(serviceId, null, publicKey, secretKey); System.out.println("Environment deleted for service: " + serviceId); break; default: System.err.println("Error: Unknown env action: " + action); System.err.println("Valid actions: status, set, export, delete"); System.exit(2); } } private static void handleSnapshot( List args, String publicKey, String secretKey ) throws Exception { // Parse snapshot-specific options boolean list = false; String infoId = null; String deleteId = null; String lockId = null; String unlockId = null; String cloneId = null; String cloneName = null; String cloneType = null; String clonePorts = null; int i = 1; // Skip "snapshot" command while (i < args.size()) { String arg = args.get(i); if (arg.equals("--list") || arg.equals("-l")) { list = true; i++; } else if (arg.equals("--info")) { if (i + 1 >= args.size()) { System.err.println("Error: --info requires an ID"); System.exit(2); } infoId = args.get(++i); i++; } else if (arg.equals("--delete")) { if (i + 1 >= args.size()) { System.err.println("Error: --delete requires an ID"); System.exit(2); } deleteId = args.get(++i); i++; } else if (arg.equals("--lock")) { if (i + 1 >= args.size()) { System.err.println("Error: --lock requires an ID"); System.exit(2); } lockId = args.get(++i); i++; } else if (arg.equals("--unlock")) { if (i + 1 >= args.size()) { System.err.println("Error: --unlock requires an ID"); System.exit(2); } unlockId = args.get(++i); i++; } else if (arg.equals("--clone")) { if (i + 1 >= args.size()) { System.err.println("Error: --clone requires an ID"); System.exit(2); } cloneId = args.get(++i); i++; } else if (arg.equals("--name")) { if (i + 1 >= args.size()) { System.err.println("Error: --name requires a value"); System.exit(2); } cloneName = args.get(++i); i++; } else if (arg.equals("--type")) { if (i + 1 >= args.size()) { System.err.println("Error: --type requires a value"); System.exit(2); } cloneType = args.get(++i); i++; } else if (arg.equals("--ports")) { if (i + 1 >= args.size()) { System.err.println("Error: --ports requires a value"); System.exit(2); } clonePorts = args.get(++i); i++; } else { i++; } } if (list) { List> snapshots = listSnapshots(publicKey, secretKey); printSnapshotList(snapshots); } else if (infoId != null) { // Get snapshot info via restore API (or just list and filter) List> snapshots = listSnapshots(publicKey, secretKey); for (Map snap : snapshots) { Object id = snap.get("id"); if (id != null && id.toString().equals(infoId)) { printMap(snap); return; } } System.err.println("Error: Snapshot not found: " + infoId); System.exit(1); } else if (deleteId != null) { deleteSnapshot(deleteId, publicKey, secretKey); System.out.println("Snapshot deleted: " + deleteId); } else if (lockId != null) { lockSnapshot(lockId, publicKey, secretKey); System.out.println("Snapshot locked: " + lockId); } else if (unlockId != null) { unlockSnapshot(unlockId, publicKey, secretKey); System.out.println("Snapshot unlocked: " + unlockId); } else if (cloneId != null) { Map result = cloneSnapshot(cloneId, cloneName, publicKey, secretKey); System.out.println("Snapshot cloned:"); printMap(result); } else { System.err.println("Error: No snapshot action specified. Use --list, --info, --delete, etc."); System.exit(2); } } private static void handleImage( List args, String publicKey, String secretKey ) throws Exception { // Parse image-specific options boolean list = false; String infoId = null; String deleteId = null; String lockId = null; String unlockId = null; String publishId = null; String sourceType = null; String visibilityId = null; String visibilityMode = null; String spawnId = null; String cloneId = null; String name = null; String ports = null; int i = 1; // Skip "image" command while (i < args.size()) { String arg = args.get(i); if (arg.equals("--list") || arg.equals("-l")) { list = true; i++; } else if (arg.equals("--info")) { if (i + 1 >= args.size()) { System.err.println("Error: --info requires an ID"); System.exit(2); } infoId = args.get(++i); i++; } else if (arg.equals("--delete")) { if (i + 1 >= args.size()) { System.err.println("Error: --delete requires an ID"); System.exit(2); } deleteId = args.get(++i); i++; } else if (arg.equals("--lock")) { if (i + 1 >= args.size()) { System.err.println("Error: --lock requires an ID"); System.exit(2); } lockId = args.get(++i); i++; } else if (arg.equals("--unlock")) { if (i + 1 >= args.size()) { System.err.println("Error: --unlock requires an ID"); System.exit(2); } unlockId = args.get(++i); i++; } else if (arg.equals("--publish")) { if (i + 1 >= args.size()) { System.err.println("Error: --publish requires an ID"); System.exit(2); } publishId = args.get(++i); i++; } else if (arg.equals("--source-type")) { if (i + 1 >= args.size()) { System.err.println("Error: --source-type requires a value"); System.exit(2); } sourceType = args.get(++i); i++; } else if (arg.equals("--visibility")) { if (i + 2 >= args.size()) { System.err.println("Error: --visibility requires ID and MODE"); System.exit(2); } visibilityId = args.get(++i); visibilityMode = args.get(++i); i++; } else if (arg.equals("--spawn")) { if (i + 1 >= args.size()) { System.err.println("Error: --spawn requires an ID"); System.exit(2); } spawnId = args.get(++i); i++; } else if (arg.equals("--clone")) { if (i + 1 >= args.size()) { System.err.println("Error: --clone requires an ID"); System.exit(2); } cloneId = args.get(++i); i++; } else if (arg.equals("--name")) { if (i + 1 >= args.size()) { System.err.println("Error: --name requires a value"); System.exit(2); } name = args.get(++i); i++; } else if (arg.equals("--ports")) { if (i + 1 >= args.size()) { System.err.println("Error: --ports requires a value"); System.exit(2); } ports = args.get(++i); i++; } else { i++; } } if (list) { List> images = listImages(null, publicKey, secretKey); printImageList(images); } else if (infoId != null) { Map image = getImage(infoId, publicKey, secretKey); printMap(image); } else if (deleteId != null) { deleteImage(deleteId, publicKey, secretKey); System.out.println("Image deleted: " + deleteId); } else if (lockId != null) { lockImage(lockId, publicKey, secretKey); System.out.println("Image locked: " + lockId); } else if (unlockId != null) { unlockImage(unlockId, publicKey, secretKey); System.out.println("Image unlocked: " + unlockId); } else if (publishId != null) { if (sourceType == null) { System.err.println("Error: --publish requires --source-type (service or snapshot)"); System.exit(2); } Map result = publishImage(sourceType, publishId, name, publicKey, secretKey); System.out.println("Image published:"); printMap(result); } else if (visibilityId != null) { if (visibilityMode == null) { System.err.println("Error: --visibility requires a mode (private, unlisted, or public)"); System.exit(2); } setImageVisibility(visibilityId, visibilityMode, publicKey, secretKey); System.out.println("Image visibility set to " + visibilityMode + ": " + visibilityId); } else if (spawnId != null) { String svcName = name != null ? name : "spawned-service"; Map result = spawnFromImage(spawnId, svcName, ports, null, null, publicKey, secretKey); System.out.println("Service spawned:"); printMap(result); } else if (cloneId != null) { String imgName = name != null ? name : cloneId + "-clone"; Map result = cloneImage(cloneId, imgName, null, publicKey, secretKey); System.out.println("Image cloned:"); printMap(result); } else { System.err.println("Error: No image action specified. Use --list, --info, --delete, --publish, etc."); System.exit(2); } } private static void printImageList(List> images) { System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "VISIBILITY", "CREATED"); for (Map image : images) { String id = getStr(image, "image_id", "id"); String name = getStr(image, "name", ""); String visibility = getStr(image, "visibility", ""); String created = getStr(image, "created_at", "created"); System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, visibility, created); } } private static void handleKey(String publicKey, String secretKey) throws Exception { Map result = validateKeys(publicKey, secretKey); printMap(result); } private static void handleLanguages(List args, String publicKey, String secretKey) throws Exception { boolean jsonOutput = false; // Parse options int i = 1; // Skip "languages" command while (i < args.size()) { String arg = args.get(i); if (arg.equals("--json")) { jsonOutput = true; } i++; } List languages = getLanguages(publicKey, secretKey); if (jsonOutput) { // Output as JSON array StringBuilder sb = new StringBuilder(); sb.append("["); for (int j = 0; j < languages.size(); j++) { if (j > 0) sb.append(","); sb.append("\"").append(escapeJsonString(languages.get(j))).append("\""); } sb.append("]"); System.out.println(sb.toString()); } else { // Output one language per line for (String lang : languages) { System.out.println(lang); } } } private static void printSessionList(List> sessions) { System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED"); for (Map session : sessions) { String id = getStr(session, "id", "session_id"); String name = getStr(session, "name", "container_name"); String status = getStr(session, "status", "state"); String created = getStr(session, "created_at", "created"); System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created); } } private static void printServiceList(List> services) { System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED"); for (Map service : services) { String id = getStr(service, "id", "service_id"); String name = getStr(service, "name", ""); String status = getStr(service, "state", "status"); String created = getStr(service, "created_at", "created"); System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created); } } private static void printSnapshotList(List> snapshots) { System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "TYPE", "CREATED"); for (Map snapshot : snapshots) { String id = getStr(snapshot, "id", "snapshot_id"); String name = getStr(snapshot, "name", ""); String type = getStr(snapshot, "type", "source_type"); String created = getStr(snapshot, "created_at", "created"); System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, type, created); } } private static String getStr(Map map, String key1, String key2) { Object val = map.get(key1); if (val != null) { return val.toString(); } val = map.get(key2); if (val != null) { return val.toString(); } return ""; } private static void printMap(Map map) { for (Map.Entry entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } } }