From af995cdc1aff0b62e1314d04049cd395d6ef2cb9 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sat, 17 Jan 2026 10:25:20 -0500 Subject: [PATCH] feat(cli): Add languages and image commands to all 42 SDKs - Add `un languages [--json]` command to list available execution languages - Add `un image` command with full image management: - --list, --info, --delete, --lock, --unlock - --publish, --visibility, --spawn, --clone - Update CLI_SPEC.md with new command documentation - All implementations follow consistent patterns --- clients/CLI_SPEC.md | 62 ++++ clients/awk/sync/src/un.awk | 400 +++++++++++++++++++++++++ clients/bash/sync/src/un.sh | 214 ++++++++++++- clients/c/src/un.c | 297 +++++++++++++++++++ clients/clojure/sync/src/un.clj | 231 ++++++++++++--- clients/cobol/sync/src/un.cob | 461 +++++++++++++++++++++++++++++ clients/cpp/sync/src/un.cpp | 219 ++++++++++++++ clients/crystal/sync/src/un.cr | 229 +++++++++++++- clients/d/sync/src/un.d | 208 +++++++++++++ clients/dart/sync/src/un.dart | 227 +++++++++++++- clients/elixir/sync/src/un.ex | 119 ++++++++ clients/erlang/sync/src/un.erl | 175 +++++++++++ clients/forth/sync/src/un.forth | 401 +++++++++++++++++++++++++ clients/fortran/sync/src/un.f90 | 309 +++++++++++++++++++ clients/fsharp/sync/src/un.fs | 187 ++++++++++++ clients/go/sync/src/un.go | 307 ++++++++++++++++++- clients/groovy/sync/src/un.groovy | 174 ++++++++++- clients/haskell/sync/src/un.hs | 164 +++++++++- clients/java/sync/src/Un.java | 254 ++++++++++++++++ clients/javascript/sync/src/un.js | 165 +++++++++++ clients/julia/sync/src/un.jl | 158 +++++++++- clients/kotlin/sync/src/un.kt | 196 +++++++++++- clients/lisp/sync/src/un.lisp | 156 ++++++++++ clients/lua/sync/src/un.lua | 210 ++++++++++++- clients/nim/sync/src/un.nim | 202 +++++++++++++ clients/objective-c/sync/src/un.m | 203 +++++++++++++ clients/ocaml/sync/src/un.ml | 122 ++++++++ clients/perl/sync/src/un.pl | 164 +++++++++- clients/php/sync/src/un.php | 251 ++++++++++++++++ clients/powershell/sync/src/un.ps1 | 162 ++++++++++ clients/prolog/sync/src/un.pro | 178 +++++++++++ clients/python/sync/src/un.py | 131 ++++++++ clients/r/sync/src/un.r | 215 +++++++++++++- clients/raku/sync/src/un.raku | 218 ++++++++++++++ clients/ruby/sync/src/un.rb | 223 ++++++++++++++ clients/rust/sync/src/lib.rs | 314 +++++++++++++++++++- clients/scheme/sync/src/un.scm | 170 +++++++++++ clients/swift/sync/src/un.swift | 146 ++++++++- clients/tcl/sync/src/un.tcl | 239 +++++++++++++++ clients/typescript/sync/src/un.ts | 199 ++++++++++++- clients/v/sync/src/un.v | 259 ++++++++++++++++ clients/zig/sync/src/un.zig | 249 ++++++++++++++++ 42 files changed, 9098 insertions(+), 70 deletions(-) diff --git a/clients/CLI_SPEC.md b/clients/CLI_SPEC.md index 59fa952..137d74c 100644 --- a/clients/CLI_SPEC.md +++ b/clients/CLI_SPEC.md @@ -58,6 +58,8 @@ un [options] # Execute code file un session [options] # Interactive session un service [options] # Manage services un snapshot [options] # Manage snapshots +un image [options] # Manage images +un languages [options] # List available languages un key # Check API key ``` @@ -204,6 +206,66 @@ un snapshot --clone ID --type service --name myapp --ports 80 | `--shell SHELL` | Shell for cloned session | | `--ports PORTS` | Ports for cloned service | +## Image Command + +```bash +un image --list # List all images +un image --info ID # Get image details +un image --delete ID # Delete image +un image --lock ID # Prevent deletion +un image --unlock ID # Allow deletion +un image --publish ID # Publish from service/snapshot +un image --visibility ID public # Set visibility (private/unlisted/public) +un image --spawn ID # Spawn new service from image +un image --clone ID # Clone image +``` + +### Image Options + +| Option | Description | +|--------|-------------| +| `--list`, `-l` | List all images | +| `--info ID` | Get image details | +| `--delete ID` | Delete image | +| `--lock ID` | Prevent deletion | +| `--unlock ID` | Allow deletion | +| `--publish ID` | Publish image from service or snapshot | +| `--visibility ID MODE` | Set visibility: private, unlisted, or public | +| `--spawn ID` | Spawn new service from image | +| `--clone ID` | Clone image | +| `--name NAME` | Name for spawned/cloned resource | +| `--ports PORTS` | Ports for spawned service | +| `--source-type TYPE` | Source type: service or snapshot (for publish) | + +## Languages Command + +```bash +un languages # List all available languages (one per line) +un languages --json # Output as JSON array for scripts +``` + +### Languages Options + +| Option | Description | +|--------|-------------| +| `--json` | Output as JSON array instead of one per line | + +The languages command fetches the list from the API and caches it in `~/.unsandbox/languages.json` for 1 hour. + +**Default output** (one language per line, suitable for piping): +``` +python +javascript +go +rust +... +``` + +**JSON output** (`--json`): +```json +["python", "javascript", "go", "rust", ...] +``` + ## Key Command ```bash diff --git a/clients/awk/sync/src/un.awk b/clients/awk/sync/src/un.awk index 0627a0f..8bee959 100644 --- a/clients/awk/sync/src/un.awk +++ b/clients/awk/sync/src/un.awk @@ -637,6 +637,55 @@ function cmd_key(do_extend) { validate_key(do_extend) } +function languages_list(json_output , timestamp, sig_headers, signature, sig_input, sig_cmd, line, response, i, lang) { + get_api_keys() + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":GET:/languages:" + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'" + } + cmd = "curl -s '" API_BASE "/languages' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers + response = "" + while ((cmd | getline line) > 0) { + response = response line + } + close(cmd) + + if (json_output) { + # Output raw JSON array of language names + # Extract languages array and convert to simple array + if (match(response, /"languages":\[([^\]]*)\]/, arr)) { + # Parse out language names from the array + content = arr[1] + printf "[" + first = 1 + while (match(content, /"name":"([^"]*)"/, m)) { + if (!first) printf "," + printf "\"%s\"", m[1] + first = 0 + content = substr(content, RSTART + RLENGTH) + } + printf "]\n" + } else { + # Fallback: just print raw response + print response + } + } else { + # Output one language per line + if (match(response, /"languages":\[([^\]]*)\]/, arr)) { + content = arr[1] + while (match(content, /"name":"([^"]*)"/, m)) { + print m[1] + content = substr(content, RSTART + RLENGTH) + } + } + } +} + function snapshot_list( timestamp, sig_headers, signature, sig_input, sig_cmd) { get_api_keys() timestamp = systime() @@ -687,6 +736,244 @@ function snapshot_delete(id , timestamp, sig_headers, signature, sig_input, s print GREEN "Snapshot deleted: " id RESET } +# Image functions +function image_list( timestamp, sig_headers, signature, sig_input, sig_cmd) { + get_api_keys() + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":GET:/images:" + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'" + } + cmd = "curl -s '" API_BASE "/images' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers + while ((cmd | getline line) > 0) print line + close(cmd) +} + +function image_info(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) { + get_api_keys() + endpoint = "/images/" id + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":GET:" endpoint ":" + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'" + } + cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers + while ((cmd | getline line) > 0) print line + close(cmd) +} + +function image_delete(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) { + get_api_keys() + endpoint = "/images/" id + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":DELETE:" endpoint ":" + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'" + } + cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers + system(cmd) + print GREEN "Image deleted: " id RESET +} + +function image_lock(id , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) { + get_api_keys() + endpoint = "/images/" id "/lock" + json = "{}" + tmp = "/tmp/un_awk_img_" PROCINFO["pid"] ".json" + print json > tmp + close(tmp) + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":POST:" endpoint ":" json + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' " + } + cmd = "curl -s -X POST '" API_BASE endpoint "' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \ + sig_headers \ + "-d '@" tmp "'" + system(cmd " > /dev/null") + system("rm -f " tmp) + print GREEN "Image locked: " id RESET +} + +function image_unlock(id , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) { + get_api_keys() + endpoint = "/images/" id "/unlock" + json = "{}" + tmp = "/tmp/un_awk_img_" PROCINFO["pid"] ".json" + print json > tmp + close(tmp) + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":POST:" endpoint ":" json + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' " + } + cmd = "curl -s -X POST '" API_BASE endpoint "' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \ + sig_headers \ + "-d '@" tmp "'" + system(cmd " > /dev/null") + system("rm -f " tmp) + print GREEN "Image unlocked: " id RESET +} + +function image_publish(source_id, source_type, name , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) { + get_api_keys() + endpoint = "/images/publish" + json = "{\"source_type\":\"" source_type "\",\"source_id\":\"" source_id "\"" + if (name != "") { + json = json ",\"name\":\"" escape_json(name) "\"" + } + json = json "}" + tmp = "/tmp/un_awk_img_" PROCINFO["pid"] ".json" + print json > tmp + close(tmp) + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":POST:" endpoint ":" json + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' " + } + cmd = "curl -s -X POST '" API_BASE endpoint "' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \ + sig_headers \ + "-d '@" tmp "'" + response = "" + while ((cmd | getline line) > 0) { + response = response line + } + close(cmd) + system("rm -f " tmp) + print GREEN "Image published" RESET + print response +} + +function image_visibility(id, visibility , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) { + get_api_keys() + endpoint = "/images/" id "/visibility" + json = "{\"visibility\":\"" visibility "\"}" + tmp = "/tmp/un_awk_img_" PROCINFO["pid"] ".json" + print json > tmp + close(tmp) + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":POST:" endpoint ":" json + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' " + } + cmd = "curl -s -X POST '" API_BASE endpoint "' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \ + sig_headers \ + "-d '@" tmp "'" + system(cmd " > /dev/null") + system("rm -f " tmp) + print GREEN "Image visibility set to: " visibility RESET +} + +function image_spawn(id, name, ports , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) { + get_api_keys() + endpoint = "/images/" id "/spawn" + json = "{" + if (name != "") { + json = json "\"name\":\"" escape_json(name) "\"" + } + if (ports != "") { + if (name != "") json = json "," + json = json "\"ports\":[" ports "]" + } + json = json "}" + tmp = "/tmp/un_awk_img_" PROCINFO["pid"] ".json" + print json > tmp + close(tmp) + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":POST:" endpoint ":" json + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' " + } + cmd = "curl -s -X POST '" API_BASE endpoint "' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \ + sig_headers \ + "-d '@" tmp "'" + response = "" + while ((cmd | getline line) > 0) { + response = response line + } + close(cmd) + system("rm -f " tmp) + print GREEN "Service spawned from image" RESET + print response +} + +function image_clone(id, name , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) { + get_api_keys() + endpoint = "/images/" id "/clone" + json = "{" + if (name != "") { + json = json "\"name\":\"" escape_json(name) "\"" + } + json = json "}" + tmp = "/tmp/un_awk_img_" PROCINFO["pid"] ".json" + print json > tmp + close(tmp) + timestamp = systime() + sig_headers = "" + if (GLOBAL_SECRET_KEY != "") { + sig_input = timestamp ":POST:" endpoint ":" json + sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'" + sig_cmd | getline signature + close(sig_cmd) + sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' " + } + cmd = "curl -s -X POST '" API_BASE endpoint "' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \ + sig_headers \ + "-d '@" tmp "'" + response = "" + while ((cmd | getline line) > 0) { + response = response line + } + close(cmd) + system("rm -f " tmp) + print GREEN "Image cloned" RESET + print response +} + function session_snapshot(id, name, hot , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) { get_api_keys() endpoint = "/sessions/" id "/snapshot" @@ -1010,6 +1297,7 @@ function service_env_delete(id , endpoint, timestamp, sig_headers, signature, function show_help() { print "Usage: awk -f un.awk " + print " awk -f un.awk languages [--json]" print " awk -f un.awk session --list" print " awk -f un.awk session --kill ID" print " awk -f un.awk session [-s SHELL] [-f FILE]..." @@ -1030,6 +1318,15 @@ function show_help() { print " awk -f un.awk snapshot --list" print " awk -f un.awk snapshot --info ID" print " awk -f un.awk snapshot --delete ID" + print " awk -f un.awk image --list" + print " awk -f un.awk image --info ID" + print " awk -f un.awk image --delete ID" + print " awk -f un.awk image --lock ID" + print " awk -f un.awk image --unlock ID" + print " awk -f un.awk image --publish ID --source-type TYPE [--name NAME]" + print " awk -f un.awk image --visibility ID MODE" + print " awk -f un.awk image --spawn ID [--name NAME] [--ports PORTS]" + print " awk -f un.awk image --clone ID [--name NAME]" print "" print "Session options:" print " -s, --shell SHELL Shell to use (default: bash)" @@ -1063,11 +1360,28 @@ function show_help() { print " export ID Export vault contents" print " delete ID Delete vault" print "" + print "Languages options:" + print " --json Output as JSON array (for scripts)" + print "" print "Snapshot options:" print " -l, --list List all snapshots" print " --info ID Get snapshot details" print " --delete ID Delete a snapshot" print "" + print "Image options:" + print " -l, --list List all images" + print " --info ID Get image details" + print " --delete ID Delete an image" + print " --lock ID Lock image to prevent deletion" + print " --unlock ID Unlock image" + print " --publish ID Publish image from service/snapshot (requires --source-type)" + print " --source-type TYPE Source type: service or snapshot" + print " --visibility ID MODE Set visibility: private, unlisted, or public" + print " --spawn ID Spawn new service from image" + print " --clone ID Clone an image" + print " --name NAME Name for spawned service or cloned image" + print " --ports PORTS Ports for spawned service" + print "" print "Requires: UNSANDBOX_API_KEY environment variable" } @@ -1159,6 +1473,15 @@ END { exit 0 } + if (ARGV[1] == "languages") { + json_output = 0 + if (ARGC >= 3 && ARGV[2] == "--json") { + json_output = 1 + } + languages_list(json_output) + exit 0 + } + if (ARGV[1] == "snapshot") { if (ARGC >= 3 && (ARGV[2] == "--list" || ARGV[2] == "-l")) { snapshot_list() @@ -1172,6 +1495,83 @@ END { exit 0 } + if (ARGV[1] == "image") { + if (ARGC >= 3 && (ARGV[2] == "--list" || ARGV[2] == "-l")) { + image_list() + } else if (ARGC >= 4 && ARGV[2] == "--info") { + image_info(ARGV[3]) + } else if (ARGC >= 4 && ARGV[2] == "--delete") { + image_delete(ARGV[3]) + } else if (ARGC >= 4 && ARGV[2] == "--lock") { + image_lock(ARGV[3]) + } else if (ARGC >= 4 && ARGV[2] == "--unlock") { + image_unlock(ARGV[3]) + } else if (ARGC >= 4 && ARGV[2] == "--publish") { + # Parse --source-type and --name options + img_source_id = ARGV[3] + img_source_type = "" + img_name = "" + i = 4 + while (i < ARGC) { + if (ARGV[i] == "--source-type" && i + 1 < ARGC) { + img_source_type = ARGV[i + 1] + i += 2 + } else if (ARGV[i] == "--name" && i + 1 < ARGC) { + img_name = ARGV[i + 1] + i += 2 + } else { + i++ + } + } + if (img_source_type == "") { + print RED "Error: --publish requires --source-type (service or snapshot)" RESET > "/dev/stderr" + exit 1 + } + image_publish(img_source_id, img_source_type, img_name) + } else if (ARGC >= 5 && ARGV[2] == "--visibility") { + image_visibility(ARGV[3], ARGV[4]) + } else if (ARGC >= 4 && ARGV[2] == "--spawn") { + # Parse --name and --ports options + img_id = ARGV[3] + img_name = "" + img_ports = "" + i = 4 + while (i < ARGC) { + if (ARGV[i] == "--name" && i + 1 < ARGC) { + img_name = ARGV[i + 1] + i += 2 + } else if (ARGV[i] == "--ports" && i + 1 < ARGC) { + img_ports = ARGV[i + 1] + i += 2 + } else { + i++ + } + } + image_spawn(img_id, img_name, img_ports) + } else if (ARGC >= 4 && ARGV[2] == "--clone") { + # Parse --name option + img_id = ARGV[3] + img_name = "" + i = 4 + while (i < ARGC) { + if (ARGV[i] == "--name" && i + 1 < ARGC) { + img_name = ARGV[i + 1] + i += 2 + } else { + i++ + } + } + image_clone(img_id, img_name) + } else { + print "Usage: awk -f un.awk image --list|--info ID|--delete ID|--lock ID|--unlock ID" + print " awk -f un.awk image --publish ID --source-type TYPE [--name NAME]" + print " awk -f un.awk image --visibility ID MODE" + print " awk -f un.awk image --spawn ID [--name NAME] [--ports PORTS]" + print " awk -f un.awk image --clone ID [--name NAME]" + } + exit 0 + } + if (ARGV[1] == "service") { if (ARGC >= 3 && ARGV[2] == "--list") { service_list() diff --git a/clients/bash/sync/src/un.sh b/clients/bash/sync/src/un.sh index 4e21803..bd2f1a8 100644 --- a/clients/bash/sync/src/un.sh +++ b/clients/bash/sync/src/un.sh @@ -164,13 +164,219 @@ detect_language() { esac } +# Languages command +cmd_languages() { + local json_output=0 + + # Parse arguments + for arg in "$@"; do + if [ "$arg" = "--json" ]; then + json_output=1 + fi + done + + local result=$(languages) + + if [ "$json_output" -eq 1 ]; then + # JSON array output + echo "$result" | jq -c '.' + else + # One language per line (default) + echo "$result" | jq -r '.[]' + fi +} + +# Image command +cmd_image() { + local action="" + local id="" + local source_type="" + local visibility_mode="" + local name="" + local ports="" + + while [ $# -gt 0 ]; do + case "$1" in + --list|-l) + action="list" + shift + ;; + --info) + action="info" + id="$2" + shift 2 + ;; + --delete) + action="delete" + id="$2" + shift 2 + ;; + --lock) + action="lock" + id="$2" + shift 2 + ;; + --unlock) + action="unlock" + id="$2" + shift 2 + ;; + --publish) + action="publish" + id="$2" + shift 2 + ;; + --source-type) + source_type="$2" + shift 2 + ;; + --visibility) + action="visibility" + id="$2" + visibility_mode="$3" + shift 3 + ;; + --spawn) + action="spawn" + id="$2" + shift 2 + ;; + --clone) + action="clone" + id="$2" + shift 2 + ;; + --name) + name="$2" + shift 2 + ;; + --ports) + ports="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + done + + case "$action" in + list) + result=$(api_request "GET" "/images" "") + echo "$result" | jq -r '.images[] | "\(.id)\t\(.name // "-")\t\(.visibility)\t\(.created_at)"' 2>/dev/null || echo "No images found" + ;; + info) + result=$(api_request "GET" "/images/$id" "") + echo "$result" | jq . + ;; + delete) + api_request "DELETE" "/images/$id" "" + echo "Image deleted successfully" + ;; + lock) + api_request "POST" "/images/$id/lock" "{}" + echo "Image locked successfully" + ;; + unlock) + api_request "POST" "/images/$id/unlock" "{}" + echo "Image unlocked successfully" + ;; + publish) + if [ -z "$source_type" ]; then + echo "Error: --source-type required for --publish" >&2 + exit 1 + fi + local body="{\"source_type\":\"$source_type\",\"source_id\":\"$id\"" + if [ -n "$name" ]; then + body="$body,\"name\":\"$name\"" + fi + body="$body}" + result=$(api_request "POST" "/images/publish" "$body") + echo "Image published successfully" + echo "$result" | jq -r '"Image ID: \(.id)"' + ;; + visibility) + if [ -z "$visibility_mode" ]; then + echo "Error: visibility mode required" >&2 + exit 1 + fi + api_request "POST" "/images/$id/visibility" "{\"visibility\":\"$visibility_mode\"}" + echo "Image visibility set to $visibility_mode" + ;; + spawn) + local body="{" + local first=1 + if [ -n "$name" ]; then + body="$body\"name\":\"$name\"" + first=0 + fi + if [ -n "$ports" ]; then + if [ "$first" -eq 0 ]; then + body="$body," + fi + body="$body\"ports\":[$ports]" + fi + body="$body}" + result=$(api_request "POST" "/images/$id/spawn" "$body") + echo "Service spawned from image" + echo "$result" | jq -r '"Service ID: \(.id)"' + ;; + clone) + local body="{" + if [ -n "$name" ]; then + body="$body\"name\":\"$name\"" + fi + body="$body}" + result=$(api_request "POST" "/images/$id/clone" "$body") + echo "Image cloned successfully" + echo "$result" | jq -r '"Image ID: \(.id)"' + ;; + *) + echo "Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID" >&2 + exit 1 + ;; + esac +} + # CLI if [ $# -gt 0 ]; then - result=$(run "$1") - echo "$result" | jq -r '.stdout // empty' - echo "$result" | jq -r '.stderr // empty' >&2 - exit "$(echo "$result" | jq -r '.exit_code // 0')" + case "$1" in + languages) + shift + cmd_languages "$@" + ;; + image) + shift + cmd_image "$@" + ;; + *) + result=$(run "$1") + echo "$result" | jq -r '.stdout // empty' + echo "$result" | jq -r '.stderr // empty' >&2 + exit "$(echo "$result" | jq -r '.exit_code // 0')" + ;; + esac else echo "Usage: bash un.sh " >&2 + echo " bash un.sh languages [--json]" >&2 + echo " bash un.sh image [options]" >&2 + echo "" >&2 + echo "Languages options:" >&2 + echo " --json Output as JSON array" >&2 + echo "" >&2 + echo "Image options:" >&2 + echo " --list List all images" >&2 + echo " --info ID Get image details" >&2 + echo " --delete ID Delete an image" >&2 + echo " --lock ID Lock image to prevent deletion" >&2 + echo " --unlock ID Unlock image" >&2 + echo " --publish ID Publish image from service/snapshot" >&2 + echo " --source-type TYPE Source type: service or snapshot" >&2 + echo " --visibility ID MODE Set visibility: private, unlisted, public" >&2 + echo " --spawn ID Spawn new service from image" >&2 + echo " --clone ID Clone an image" >&2 + echo " --name NAME Name for spawned service or cloned image" >&2 + echo " --ports PORTS Ports for spawned service" >&2 exit 1 fi diff --git a/clients/c/src/un.c b/clients/c/src/un.c index 5b3bcb8..c4d55e1 100644 --- a/clients/c/src/un.c +++ b/clients/c/src/un.c @@ -5524,12 +5524,16 @@ void print_usage(const char *prog) { fprintf(stderr, " %s session [options]\n", prog); fprintf(stderr, " %s service [options]\n", prog); fprintf(stderr, " %s snapshot [options]\n", prog); + fprintf(stderr, " %s image [options]\n", prog); + fprintf(stderr, " %s languages [--json]\n", prog); fprintf(stderr, " %s key\n\n", prog); fprintf(stderr, "Commands:\n"); fprintf(stderr, " (default) Execute source file in sandbox\n"); fprintf(stderr, " session Open interactive shell/REPL session\n"); fprintf(stderr, " service Manage persistent services\n"); fprintf(stderr, " snapshot Manage container snapshots\n"); + fprintf(stderr, " image Manage images (publish, spawn, clone)\n"); + fprintf(stderr, " languages List available languages (--json for JSON output)\n"); fprintf(stderr, " key Check API key validity and expiration\n"); fprintf(stderr, "\nOptions:\n"); fprintf(stderr, " -s, --shell LANG Specify language (default: bash if arg is not a file)\n"); @@ -5602,6 +5606,19 @@ void print_usage(const char *prog) { fprintf(stderr, " --name NAME Name for cloned service (for --clone)\n"); fprintf(stderr, " --shell SHELL Shell for cloned session (for --clone)\n"); fprintf(stderr, " --ports PORTS Ports for cloned service (for --clone)\n"); + fprintf(stderr, "\nImage options:\n"); + fprintf(stderr, " -l, --list List all images\n"); + fprintf(stderr, " --info ID Get image details\n"); + fprintf(stderr, " --delete ID Delete an image\n"); + fprintf(stderr, " --lock ID Lock an image to prevent deletion\n"); + fprintf(stderr, " --unlock ID Unlock an image to allow deletion\n"); + fprintf(stderr, " --publish ID Publish image from service/snapshot (requires --source-type)\n"); + fprintf(stderr, " --source-type TYPE Source type: service or snapshot (for --publish)\n"); + fprintf(stderr, " --visibility ID V Set visibility: private, unlisted, or public\n"); + fprintf(stderr, " --spawn ID Spawn new service from image\n"); + fprintf(stderr, " --clone ID Clone an image\n"); + fprintf(stderr, " --name NAME Name for spawned service or cloned image\n"); + fprintf(stderr, " --ports PORTS Ports for spawned service\n"); fprintf(stderr, "\nAvailable shells/REPLs:\n"); fprintf(stderr, " Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash\n"); fprintf(stderr, " REPLs: python3, bpython, ipython, node, ruby, irb, lua, php, perl\n"); @@ -8277,6 +8294,77 @@ int main(int argc, char *argv[]) { return ret; } + // Check for languages command + if (argc >= 2 && strcmp(argv[1], "languages") == 0) { + int json_output = 0; + + // Parse options + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { + i++; + cli_public_key = argv[i]; + } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) { + i++; + cli_secret_key = argv[i]; + } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) { + i++; + cli_account_index = atoi(argv[i]); + } else if (strcmp(argv[i], "--json") == 0) { + json_output = 1; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + fprintf(stderr, "Usage: %s languages [options]\n\n", argv[0]); + fprintf(stderr, "List all available languages for code execution.\n\n"); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " --json Output as JSON array (for scripts)\n"); + fprintf(stderr, " -p KEY Public key\n"); + fprintf(stderr, " -k KEY Secret key\n"); + fprintf(stderr, " -h Show this help\n"); + return 0; + } + } + + // Get credentials + UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index); + + if (!creds || !creds->public_key || strlen(creds->public_key) == 0) { + fprintf(stderr, "Error: API credentials required.\n"); + fprintf(stderr, " Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n"); + fprintf(stderr, " Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n"); + fprintf(stderr, " Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n"); + free_credentials(creds); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + unsandbox_languages_t *langs = unsandbox_get_languages(creds->public_key, creds->secret_key); + curl_global_cleanup(); + + if (!langs) { + fprintf(stderr, "Error: Failed to fetch languages list\n"); + free_credentials(creds); + return 1; + } + + if (json_output) { + // Output as JSON array + printf("["); + for (int i = 0; i < langs->count; i++) { + printf("\"%s\"", langs->languages[i]); + if (i < langs->count - 1) printf(","); + } + printf("]\n"); + } else { + // Output one per line (pipe-friendly) + for (int i = 0; i < langs->count; i++) { + printf("%s\n", langs->languages[i]); + } + } + + unsandbox_free_languages(langs); + free_credentials(creds); + return 0; + } + // Check for snapshot command if (argc >= 2 && strcmp(argv[1], "snapshot") == 0) { const char *snapshot_id = NULL; @@ -8402,6 +8490,215 @@ int main(int argc, char *argv[]) { return ret; } + // Check for image command + if (argc >= 2 && strcmp(argv[1], "image") == 0) { + const char *image_id = NULL; + const char *visibility = NULL; + const char *spawn_name = NULL; + const char *spawn_ports = NULL; + const char *source_type = NULL; + int do_list = 0; + int do_info = 0; + int do_delete = 0; + int do_lock = 0; + int do_unlock = 0; + int do_publish = 0; + int do_visibility = 0; + int do_spawn = 0; + int do_clone = 0; + int show_help = 0; + + // Parse image-specific args + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { + i++; + cli_public_key = argv[i]; + } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) { + i++; + cli_secret_key = argv[i]; + } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) { + i++; + cli_account_index = atoi(argv[i]); + } else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0) { + do_list = 1; + } else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) { + do_info = 1; + i++; + image_id = argv[i]; + } else if (strcmp(argv[i], "--delete") == 0 && i + 1 < argc) { + do_delete = 1; + i++; + image_id = argv[i]; + } else if (strcmp(argv[i], "--lock") == 0 && i + 1 < argc) { + do_lock = 1; + i++; + image_id = argv[i]; + } else if (strcmp(argv[i], "--unlock") == 0 && i + 1 < argc) { + do_unlock = 1; + i++; + image_id = argv[i]; + } else if (strcmp(argv[i], "--publish") == 0 && i + 1 < argc) { + do_publish = 1; + i++; + image_id = argv[i]; // This is actually the source ID (service/snapshot) + } else if (strcmp(argv[i], "--visibility") == 0 && i + 2 < argc) { + do_visibility = 1; + i++; + image_id = argv[i]; + i++; + visibility = argv[i]; + } else if (strcmp(argv[i], "--spawn") == 0 && i + 1 < argc) { + do_spawn = 1; + i++; + image_id = argv[i]; + } else if (strcmp(argv[i], "--clone") == 0 && i + 1 < argc) { + do_clone = 1; + i++; + image_id = argv[i]; + } else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) { + i++; + spawn_name = argv[i]; + } else if (strcmp(argv[i], "--ports") == 0 && i + 1 < argc) { + i++; + spawn_ports = argv[i]; + } else if (strcmp(argv[i], "--source-type") == 0 && i + 1 < argc) { + i++; + source_type = argv[i]; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + show_help = 1; + } else if (argv[i][0] == '-') { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + if (show_help) { + print_usage(argv[0]); + return 0; + } + + // Get credentials + UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index); + if (!creds || !creds->public_key || strlen(creds->public_key) == 0) { + fprintf(stderr, "Error: API credentials required.\n"); + fprintf(stderr, " Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n"); + fprintf(stderr, " Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n"); + fprintf(stderr, " Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n"); + free_credentials(creds); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + int ret = 0; + + if (do_list) { + char *response = list_images(creds, NULL); + if (response) { + // Parse and display image list + unsandbox_image_list_t *images = unsandbox_image_list(creds->public_key, creds->secret_key); + if (images && images->count > 0) { + printf("%-40s %-20s %-10s %-8s %-10s\n", "ID", "NAME", "VISIBILITY", "LOCKED", "SOURCE"); + printf("%-40s %-20s %-10s %-8s %-10s\n", "----------------------------------------", + "--------------------", "----------", "--------", "----------"); + for (size_t i = 0; i < images->count; i++) { + unsandbox_image_t *img = &images->images[i]; + printf("%-40s %-20s %-10s %-8s %-10s\n", + img->id ? img->id : "", + img->name ? img->name : "", + img->visibility ? img->visibility : "", + img->locked ? "yes" : "no", + img->source_type ? img->source_type : ""); + } + unsandbox_free_image_list(images); + } else { + printf("No images found.\n"); + } + free(response); + } else { + fprintf(stderr, "Error: Failed to list images\n"); + ret = 1; + } + } else if (do_info) { + char *response = get_image(creds, image_id); + if (response) { + printf("%s\n", response); + free(response); + } else { + fprintf(stderr, "Error: Failed to get image info\n"); + ret = 1; + } + } else if (do_delete) { + ret = delete_image(creds, image_id); + if (ret == 0) { + printf("Image deleted: %s\n", image_id); + } + } else if (do_lock) { + ret = lock_image(creds, image_id); + if (ret == 0) { + printf("Image locked: %s\n", image_id); + } + } else if (do_unlock) { + ret = unlock_image(creds, image_id); + if (ret == 0) { + printf("Image unlocked: %s\n", image_id); + } + } else if (do_visibility) { + if (!visibility || (strcmp(visibility, "private") != 0 && + strcmp(visibility, "unlisted") != 0 && + strcmp(visibility, "public") != 0)) { + fprintf(stderr, "Error: visibility must be 'private', 'unlisted', or 'public'\n"); + ret = 1; + } else { + ret = set_image_visibility(creds, image_id, visibility); + if (ret == 0) { + printf("Image visibility set to %s: %s\n", visibility, image_id); + } + } + } else if (do_spawn) { + char *result = spawn_from_image(creds, image_id, spawn_name, spawn_ports); + if (result) { + printf("%s\n", result); + free(result); + } else { + fprintf(stderr, "Error: Failed to spawn from image\n"); + ret = 1; + } + } else if (do_publish) { + if (!source_type) { + fprintf(stderr, "Error: --source-type required for --publish (service or snapshot)\n"); + ret = 1; + } else { + char *result = unsandbox_image_publish(creds->public_key, creds->secret_key, + source_type, image_id, spawn_name, NULL); + if (result) { + printf("Image published: %s\n", result); + free(result); + } else { + fprintf(stderr, "Error: Failed to publish image\n"); + ret = 1; + } + } + } else if (do_clone) { + char *result = unsandbox_image_clone(creds->public_key, creds->secret_key, image_id, spawn_name); + if (result) { + printf("Image cloned: %s\n", result); + free(result); + } else { + fprintf(stderr, "Error: Failed to clone image\n"); + ret = 1; + } + } else { + fprintf(stderr, "Error: No image action specified. Use --list, --info, --delete, --lock, --unlock, --visibility, --spawn, --publish, or --clone\n"); + print_usage(argv[0]); + ret = 1; + } + + curl_global_cleanup(); + free_credentials(creds); + return ret; + } + // Check for service command first if (argc >= 2 && strcmp(argv[1], "service") == 0) { const char *service_name = NULL; diff --git a/clients/clojure/sync/src/un.clj b/clients/clojure/sync/src/un.clj index f9a320b..c6105e9 100644 --- a/clients/clojure/sync/src/un.clj +++ b/clients/clojure/sync/src/un.clj @@ -518,6 +518,96 @@ (let [api-key (get-api-key)] (validate-key api-key extend?))) +(defn languages-command [json-output?] + (let [api-key (get-api-key) + response (curl-get api-key "/languages")] + (if json-output? + ;; Extract language names and output as JSON array + (let [langs (re-seq #"\"name\":\"([^\"]+)\"" response) + names (map second langs)] + (println (str "[" (str/join "," (map #(str "\"" % "\"") names)) "]"))) + ;; Output one language per line + (let [langs (re-seq #"\"name\":\"([^\"]+)\"" response) + names (map second langs)] + (doseq [name names] + (println name)))))) + +;; Image command functions +(defn image-list [] + (let [api-key (get-api-key)] + (println (curl-get api-key "/images")))) + +(defn image-info [id] + (let [api-key (get-api-key)] + (println (curl-get api-key (str "/images/" id))))) + +(defn image-delete [id] + (let [api-key (get-api-key)] + (curl-delete api-key (str "/images/" id)) + (println (str green "Image deleted: " id reset)))) + +(defn image-lock [id] + (let [api-key (get-api-key)] + (curl-post api-key (str "/images/" id "/lock") "{}") + (println (str green "Image locked: " id reset)))) + +(defn image-unlock [id] + (let [api-key (get-api-key)] + (curl-post api-key (str "/images/" id "/unlock") "{}") + (println (str green "Image unlocked: " id reset)))) + +(defn image-publish [source-id source-type name] + (let [api-key (get-api-key) + json (str "{\"source_type\":\"" source-type "\",\"source_id\":\"" source-id "\"" + (if name (str ",\"name\":\"" (escape-json name) "\"") "") + "}")] + (println (str green "Image published" reset)) + (println (curl-post api-key "/images/publish" json)))) + +(defn image-visibility [id visibility] + (let [api-key (get-api-key) + json (str "{\"visibility\":\"" visibility "\"}")] + (curl-post api-key (str "/images/" id "/visibility") json) + (println (str green "Image visibility set to: " visibility reset)))) + +(defn image-spawn [id name ports] + (let [api-key (get-api-key) + json (str "{" + (if name (str "\"name\":\"" (escape-json name) "\"") "") + (if (and name ports) "," "") + (if ports (str "\"ports\":[" ports "]") "") + "}")] + (println (str green "Service spawned from image" reset)) + (println (curl-post api-key (str "/images/" id "/spawn") json)))) + +(defn image-clone [id name] + (let [api-key (get-api-key) + json (str "{" (if name (str "\"name\":\"" (escape-json name) "\"") "") "}")] + (println (str green "Image cloned" reset)) + (println (curl-post api-key (str "/images/" id "/clone") json)))) + +(defn image-command [action id source-type visibility name ports] + (case action + :list (image-list) + :info (image-info id) + :delete (image-delete id) + :lock (image-lock id) + :unlock (image-unlock id) + :publish (if source-type + (image-publish id source-type name) + (do + (binding [*out* *err*] + (println (str red "Error: --publish requires --source-type (service or snapshot)" reset))) + (System/exit 1))) + :visibility (if visibility + (image-visibility id visibility) + (do + (binding [*out* *err*] + (println (str red "Error: --visibility requires visibility mode (private, unlisted, public)" reset))) + (System/exit 1))) + :spawn (image-spawn id name ports) + :clone (image-clone id name))) + (defn parse-args [args] (loop [args args file nil @@ -541,6 +631,12 @@ service-envs [] service-env-file nil key-extend false + image-action nil + image-id nil + image-source-type nil + image-visibility nil + image-name nil + image-ports nil mode :execute] (cond (empty? args) @@ -548,155 +644,222 @@ :session (session-command (or session-action :create) session-id session-shell network vcpu session-input-files) :service (service-command (or service-action :create) service-id service-name service-ports service-bootstrap service-bootstrap-file service-type network vcpu service-input-files service-envs service-env-file) :key (key-command key-extend) + :languages (languages-command false) + :image (image-command (or image-action :list) image-id image-source-type image-visibility image-name image-ports) :execute (if file (execute-command file env-vars artifacts out-dir network vcpu) (do (println "Usage: un.clj [options] ") + (println " un.clj languages [--json]") (println " un.clj session [options]") (println " un.clj service [options]") (println " un.clj service env ") + (println " un.clj image [options]") (println " un.clj key [options]") (System/exit 1)))) (= (first args) "session") (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :session) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports :session) (= (first args) "service") (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :service) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports :service) (= (first args) "key") (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :key) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports :key) + + (= (first args) "languages") + (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports :languages) + + (= (first args) "image") + (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports :image) + + ;; Image options + (and (= mode :image) (or (= (first args) "--list") (= (first args) "-l"))) + (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :list image-id image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--info")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :info (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--delete")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :delete (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--lock")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :lock (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--unlock")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :unlock (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--publish")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :publish (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--source-type")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id (second args) image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--visibility")) + (recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :visibility (second args) image-source-type (nth args 2) image-name image-ports mode) + + (and (= mode :image) (= (first args) "--spawn")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :spawn (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--clone")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend :clone (second args) image-source-type image-visibility image-name image-ports mode) + + (and (= mode :image) (= (first args) "--name")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility (second args) image-ports mode) + + (and (= mode :image) (= (first args) "--ports")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name (second args) mode) ;; Key options (and (= mode :key) (= (first args) "--extend")) (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files true mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file true image-action image-id image-source-type image-visibility image-name image-ports mode) + + ;; Languages options + (and (= mode :languages) (= (first args) "--json")) + (do + (languages-command true) + (System/exit 0)) ;; Session options (and (= mode :session) (= (first args) "--list")) (recur (rest args) file env-vars artifacts out-dir network vcpu :list session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :session) (= (first args) "--kill")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu :kill (second args) session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :session) (or (= (first args) "--shell") (= (first args) "-s"))) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id (second args) session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :session) (= (first args) "-f")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell (conj session-input-files (second args)) - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) ;; Service options (and (= mode :service) (= (first args) "--list")) (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :list service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :list service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--info")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :info (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :info (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--logs")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :logs (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :logs (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--freeze")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :sleep (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :sleep (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--unfreeze")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :wake (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :wake (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--destroy")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :destroy (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :destroy (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--resize")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :resize (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :resize (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--execute")) (recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :execute (second args) service-name service-ports (nth args 2) service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :execute (second args) service-name service-ports (nth args 2) service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--dump-bootstrap") (>= (count args) 3) (not (.startsWith (nth args 2) "-"))) (recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :dump-bootstrap (second args) service-name service-ports service-bootstrap (nth args 2) service-type service-input-files service-envs service-env-file key-extend mode) + :dump-bootstrap (second args) service-name service-ports service-bootstrap (nth args 2) service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--dump-bootstrap")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :dump-bootstrap (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :dump-bootstrap (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--name")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :create service-id (second args) service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + :create service-id (second args) service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--ports")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name (second args) service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name (second args) service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--bootstrap")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports (second args) service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports (second args) service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--bootstrap-file")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap (second args) service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap (second args) service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--type")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file (second args) service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file (second args) service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "-f")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type (conj service-input-files (second args)) service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type (conj service-input-files (second args)) service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "env") (>= (count args) 2)) (let [env-action (second args) env-target (when (and (>= (count args) 3) (not (.startsWith (nth args 2) "-"))) (nth args 2)) rest-args (if env-target (drop 3 args) (drop 2 args))] (recur rest-args file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - :env env-action env-target service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode)) + :env env-action env-target service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode)) (and (= mode :service) (= (first args) "-e")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files (conj service-envs (second args)) service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files (conj service-envs (second args)) service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (and (= mode :service) (= (first args) "--env-file")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs (second args) key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs (second args) key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) ;; Execute options (= (first args) "-e") (let [[k v] (str/split (second args) #"=" 2)] (recur (rest (rest args)) file (conj env-vars [k v]) artifacts out-dir network vcpu - session-action session-id session-shell session-input-files service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode)) + session-action session-id session-shell session-input-files service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode)) (= (first args) "-a") (recur (rest args) file env-vars true out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (= (first args) "-o") (recur (rest (rest args)) file env-vars artifacts (second args) network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (= (first args) "-n") (recur (rest (rest args)) file env-vars artifacts out-dir (second args) vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) (= (first args) "-v") (recur (rest (rest args)) file env-vars artifacts out-dir network (Integer/parseInt (second args)) session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) ;; Source file (and (= mode :execute) (not (.startsWith (first args) "-")) (nil? file)) (recur (rest args) (first args) env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode) ;; Unknown option check (and (= mode :session) (.startsWith (first args) "-")) @@ -708,6 +871,6 @@ :else (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files - service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode)))) + service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend image-action image-id image-source-type image-visibility image-name image-ports mode)))) (parse-args *command-line-args*) diff --git a/clients/cobol/sync/src/un.cob b/clients/cobol/sync/src/un.cob index 78661f6..1030445 100644 --- a/clients/cobol/sync/src/un.cob +++ b/clients/cobol/sync/src/un.cob @@ -87,6 +87,11 @@ 01 WS-VCPU PIC 9(2) VALUE 0. 01 WS-VCPU-STR PIC X(8). 01 WS-RAM PIC 9(4) VALUE 0. + 01 WS-JSON-OUTPUT PIC X(8). + 01 WS-IMAGE-SOURCE-TYPE PIC X(32). + 01 WS-IMAGE-VISIBILITY PIC X(32). + 01 WS-ARG4 PIC X(256). + 01 WS-ARG5 PIC X(256). PROCEDURE DIVISION. MAIN-PROCEDURE. @@ -117,6 +122,16 @@ STOP RUN END-IF. + IF WS-ARG1 = "languages" + PERFORM HANDLE-LANGUAGES + STOP RUN + END-IF. + + IF WS-ARG1 = "image" + PERFORM HANDLE-IMAGE + STOP RUN + END-IF. + * Default: execute command MOVE WS-ARG1 TO WS-FILENAME. PERFORM HANDLE-EXECUTE. @@ -988,3 +1003,449 @@ END-STRING. CALL "SYSTEM" USING WS-CURL-CMD. + + HANDLE-LANGUAGES. + * Get API keys + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". + IF WS-PUBLIC-KEY NOT = SPACES + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY" + IF WS-SECRET-KEY = SPACES + DISPLAY "Error: UNSANDBOX_SECRET_KEY not set" + UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + ELSE + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY" + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or " + "UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + MOVE WS-API-KEY TO WS-PUBLIC-KEY + MOVE WS-API-KEY TO WS-SECRET-KEY + END-IF. + + * Parse --json flag + MOVE SPACES TO WS-JSON-OUTPUT. + ACCEPT WS-ARG2 FROM ARGUMENT-VALUE. + IF WS-ARG2 = "--json" + MOVE "true" TO WS-JSON-OUTPUT + END-IF. + + * Perform languages list + PERFORM LANGUAGES-LIST. + + LANGUAGES-LIST. + IF WS-JSON-OUTPUT = "true" + * JSON output: extract language names as array + STRING "TS=$(date +%s); " + "SIG=$(echo -n \"$TS:GET:/languages:\" | " + "openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "RESP=$(curl -s -X GET 'https://api.unsandbox.com/languages' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG); " + "echo \"$RESP\" | jq -c '[.languages[].name]'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + ELSE + * Plain output: one language per line + STRING "TS=$(date +%s); " + "SIG=$(echo -n \"$TS:GET:/languages:\" | " + "openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "RESP=$(curl -s -X GET 'https://api.unsandbox.com/languages' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG); " + "echo \"$RESP\" | jq -r '.languages[].name'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + CALL "SYSTEM" USING WS-CURL-CMD. + + HANDLE-IMAGE. + * Get API keys (try new format first, fall back to old) + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". + IF WS-PUBLIC-KEY NOT = SPACES + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY" + IF WS-SECRET-KEY = SPACES + DISPLAY "Error: UNSANDBOX_SECRET_KEY not set" + UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + ELSE + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY" + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or " + "UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + MOVE WS-API-KEY TO WS-PUBLIC-KEY + MOVE WS-API-KEY TO WS-SECRET-KEY + END-IF. + + * Initialize image parameters + MOVE SPACES TO WS-ID. + MOVE SPACES TO WS-NAME. + MOVE SPACES TO WS-PORTS. + MOVE SPACES TO WS-IMAGE-SOURCE-TYPE. + MOVE SPACES TO WS-IMAGE-VISIBILITY. + + * Parse image arguments + ACCEPT WS-ARG2 FROM ARGUMENT-VALUE. + + IF WS-ARG2 = "-l" OR WS-ARG2 = "--list" + PERFORM IMAGE-LIST + ELSE IF WS-ARG2 = "--info" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM IMAGE-INFO + ELSE IF WS-ARG2 = "--delete" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM IMAGE-DELETE + ELSE IF WS-ARG2 = "--lock" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM IMAGE-LOCK + ELSE IF WS-ARG2 = "--unlock" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM IMAGE-UNLOCK + ELSE IF WS-ARG2 = "--publish" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM PARSE-IMAGE-PUBLISH-ARGS + PERFORM IMAGE-PUBLISH + ELSE IF WS-ARG2 = "--visibility" + ACCEPT WS-ID FROM ARGUMENT-VALUE + ACCEPT WS-IMAGE-VISIBILITY FROM ARGUMENT-VALUE + PERFORM IMAGE-VISIBILITY + ELSE IF WS-ARG2 = "--spawn" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM PARSE-IMAGE-SPAWN-ARGS + PERFORM IMAGE-SPAWN + ELSE IF WS-ARG2 = "--clone" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM PARSE-IMAGE-CLONE-ARGS + PERFORM IMAGE-CLONE + ELSE + DISPLAY "Error: Use --list, --info, --delete, " + "--lock, --unlock, --publish, --visibility, " + "--spawn, or --clone" UPON SYSERR + MOVE 1 TO RETURN-CODE + END-IF. + + IMAGE-LIST. + STRING "TS=$(date +%s); " + "SIG=$(echo -n \"$TS:GET:/images:\" | " + "openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X GET 'https://api.unsandbox.com/images' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG | jq ." + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + IMAGE-INFO. + STRING "TS=$(date +%s); " + "SIG=$(echo -n \"$TS:GET:/images/" + FUNCTION TRIM(WS-ID) + ":\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X GET 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG | jq ." + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + IMAGE-DELETE. + STRING "TS=$(date +%s); " + "SIG=$(echo -n \"$TS:DELETE:/images/" + FUNCTION TRIM(WS-ID) + ":\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X DELETE 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG >/dev/null && " + "echo -e '\x1b[32mImage deleted: " + FUNCTION TRIM(WS-ID) "\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + IMAGE-LOCK. + STRING "TS=$(date +%s); " + "BODY='{}'; " + "SIG=$(echo -n \"$TS:POST:/images/" + FUNCTION TRIM(WS-ID) + "/lock:$BODY\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X POST 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "/lock' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG " + "-d \"$BODY\" >/dev/null && " + "echo -e '\x1b[32mImage locked: " + FUNCTION TRIM(WS-ID) "\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + IMAGE-UNLOCK. + STRING "TS=$(date +%s); " + "BODY='{}'; " + "SIG=$(echo -n \"$TS:POST:/images/" + FUNCTION TRIM(WS-ID) + "/unlock:$BODY\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X POST 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "/unlock' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG " + "-d \"$BODY\" >/dev/null && " + "echo -e '\x1b[32mImage unlocked: " + FUNCTION TRIM(WS-ID) "\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + PARSE-IMAGE-PUBLISH-ARGS. + * Parse --source-type and --name for publish + MOVE SPACES TO WS-IMAGE-SOURCE-TYPE. + MOVE SPACES TO WS-NAME. + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE. + PERFORM UNTIL WS-ARG3 = SPACES + IF WS-ARG3 = "--source-type" + ACCEPT WS-IMAGE-SOURCE-TYPE FROM ARGUMENT-VALUE + ELSE IF WS-ARG3 = "--name" + ACCEPT WS-NAME FROM ARGUMENT-VALUE + END-IF + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE + END-PERFORM. + + IMAGE-PUBLISH. + * Validate source-type + IF WS-IMAGE-SOURCE-TYPE = SPACES + DISPLAY "Error: --publish requires --source-type " + "(service or snapshot)" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF. + + * Build publish request + STRING "TS=$(date +%s); " + "BODY='{\"source_type\":\"" + FUNCTION TRIM(WS-IMAGE-SOURCE-TYPE) + "\",\"source_id\":\"" + FUNCTION TRIM(WS-ID) "\"" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + IF WS-NAME NOT = SPACES + STRING FUNCTION TRIM(WS-CURL-CMD) + ",\"name\":\"" FUNCTION TRIM(WS-NAME) "\"" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + STRING FUNCTION TRIM(WS-CURL-CMD) + "}'; " + "SIG=$(echo -n \"$TS:POST:/images/publish:$BODY\" | " + "openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X POST 'https://api.unsandbox.com/images/publish' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG " + "-d \"$BODY\" && " + "echo -e '\x1b[32mImage published\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + IMAGE-VISIBILITY. + STRING "TS=$(date +%s); " + "BODY='{\"visibility\":\"" + FUNCTION TRIM(WS-IMAGE-VISIBILITY) + "\"}'; " + "SIG=$(echo -n \"$TS:POST:/images/" + FUNCTION TRIM(WS-ID) + "/visibility:$BODY\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X POST 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "/visibility' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG " + "-d \"$BODY\" >/dev/null && " + "echo -e '\x1b[32mImage visibility set to: " + FUNCTION TRIM(WS-IMAGE-VISIBILITY) "\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + PARSE-IMAGE-SPAWN-ARGS. + * Parse --name and --ports for spawn + MOVE SPACES TO WS-NAME. + MOVE SPACES TO WS-PORTS. + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE. + PERFORM UNTIL WS-ARG3 = SPACES + IF WS-ARG3 = "--name" + ACCEPT WS-NAME FROM ARGUMENT-VALUE + ELSE IF WS-ARG3 = "--ports" + ACCEPT WS-PORTS FROM ARGUMENT-VALUE + END-IF + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE + END-PERFORM. + + IMAGE-SPAWN. + * Build spawn request + STRING "TS=$(date +%s); " + "BODY='{" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + IF WS-NAME NOT = SPACES + STRING FUNCTION TRIM(WS-CURL-CMD) + "\"name\":\"" FUNCTION TRIM(WS-NAME) "\"" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + IF WS-PORTS NOT = SPACES + IF WS-NAME NOT = SPACES + STRING FUNCTION TRIM(WS-CURL-CMD) "," + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF + STRING FUNCTION TRIM(WS-CURL-CMD) + "\"ports\":[" FUNCTION TRIM(WS-PORTS) "]" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + STRING FUNCTION TRIM(WS-CURL-CMD) + "}'; " + "SIG=$(echo -n \"$TS:POST:/images/" + FUNCTION TRIM(WS-ID) + "/spawn:$BODY\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X POST 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "/spawn' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG " + "-d \"$BODY\" && " + "echo -e '\x1b[32mService spawned from image\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + + PARSE-IMAGE-CLONE-ARGS. + * Parse --name for clone + MOVE SPACES TO WS-NAME. + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE. + PERFORM UNTIL WS-ARG3 = SPACES + IF WS-ARG3 = "--name" + ACCEPT WS-NAME FROM ARGUMENT-VALUE + END-IF + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE + END-PERFORM. + + IMAGE-CLONE. + * Build clone request + STRING "TS=$(date +%s); " + "BODY='{" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + IF WS-NAME NOT = SPACES + STRING FUNCTION TRIM(WS-CURL-CMD) + "\"name\":\"" FUNCTION TRIM(WS-NAME) "\"" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + STRING FUNCTION TRIM(WS-CURL-CMD) + "}'; " + "SIG=$(echo -n \"$TS:POST:/images/" + FUNCTION TRIM(WS-ID) + "/clone:$BODY\" | openssl dgst -sha256 -hmac '" + FUNCTION TRIM(WS-SECRET-KEY) + "' | cut -d' ' -f2); " + "curl -s -X POST 'https://api.unsandbox.com/images/" + FUNCTION TRIM(WS-ID) + "/clone' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + FUNCTION TRIM(WS-PUBLIC-KEY) + "' " + "-H 'X-Timestamp: '$TS " + "-H 'X-Signature: '$SIG " + "-d \"$BODY\" && " + "echo -e '\x1b[32mImage cloned\x1b[0m'" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. diff --git a/clients/cpp/sync/src/un.cpp b/clients/cpp/sync/src/un.cpp index 067f56f..ada4aa7 100644 --- a/clients/cpp/sync/src/un.cpp +++ b/clients/cpp/sync/src/un.cpp @@ -694,6 +694,181 @@ void cmd_service(const string& name, const string& ports, const string& type, co exit(1); } +void cmd_languages(bool json_output, const string& public_key, const string& secret_key) { + string auth_headers = build_auth_headers("GET", "/languages", "", public_key, secret_key); + string cmd = "curl -s -X GET '" + API_BASE + "/languages' " + auth_headers; + string result = exec_curl(cmd); + + if (json_output) { + // Extract language names and output as JSON array + vector names; + size_t pos = 0; + string search = "\"name\":\""; + while ((pos = result.find(search, pos)) != string::npos) { + pos += search.length(); + size_t end = result.find("\"", pos); + if (end != string::npos) { + names.push_back(result.substr(pos, end - pos)); + pos = end; + } + } + cout << "["; + for (size_t i = 0; i < names.size(); i++) { + if (i > 0) cout << ","; + cout << "\"" << names[i] << "\""; + } + cout << "]" << endl; + } else { + // Output one language per line + size_t pos = 0; + string search = "\"name\":\""; + while ((pos = result.find(search, pos)) != string::npos) { + pos += search.length(); + size_t end = result.find("\"", pos); + if (end != string::npos) { + cout << result.substr(pos, end - pos) << endl; + pos = end; + } + } + } +} + +void cmd_image(bool list, const string& info, const string& del, const string& lock, const string& unlock, + const string& publish, const string& source_type, const string& visibility_id, const string& visibility, + const string& spawn, const string& clone, const string& name, const string& ports, + const string& public_key, const string& secret_key) { + if (list) { + string auth_headers = build_auth_headers("GET", "/images", "", public_key, secret_key); + string cmd = "curl -s -X GET '" + API_BASE + "/images' " + auth_headers; + cout << exec_curl(cmd) << endl; + return; + } + + if (!info.empty()) { + string path = "/images/" + info; + string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key); + string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers; + cout << exec_curl(cmd) << endl; + return; + } + + if (!del.empty()) { + string path = "/images/" + del; + string auth_headers = build_auth_headers("DELETE", path, "", public_key, secret_key); + string cmd = "curl -s -X DELETE '" + API_BASE + path + "' " + auth_headers; + exec_curl(cmd); + cout << GREEN << "Image deleted: " << del << RESET << endl; + return; + } + + if (!lock.empty()) { + string path = "/images/" + lock + "/lock"; + string body = "{}"; + string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key); + string cmd = "curl -s -X POST '" + API_BASE + path + "' " + "-H 'Content-Type: application/json' " + + auth_headers + " -d '" + body + "'"; + exec_curl(cmd); + cout << GREEN << "Image locked: " << lock << RESET << endl; + return; + } + + if (!unlock.empty()) { + string path = "/images/" + unlock + "/unlock"; + string body = "{}"; + string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key); + string cmd = "curl -s -X POST '" + API_BASE + path + "' " + "-H 'Content-Type: application/json' " + + auth_headers + " -d '" + body + "'"; + exec_curl(cmd); + cout << GREEN << "Image unlocked: " << unlock << RESET << endl; + return; + } + + if (!publish.empty()) { + if (source_type.empty()) { + cerr << RED << "Error: --publish requires --source-type (service or snapshot)" << RESET << endl; + exit(1); + } + ostringstream json; + json << "{\"source_type\":\"" << source_type << "\",\"source_id\":\"" << publish << "\""; + if (!name.empty()) { + json << ",\"name\":\"" << escape_json(name) << "\""; + } + json << "}"; + string path = "/images/publish"; + string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key); + string cmd = "curl -s -X POST '" + API_BASE + path + "' " + "-H 'Content-Type: application/json' " + + auth_headers + " -d '" + json.str() + "'"; + cout << GREEN << "Image published" << RESET << endl; + cout << exec_curl(cmd) << endl; + return; + } + + if (!visibility_id.empty()) { + if (visibility.empty()) { + cerr << RED << "Error: --visibility requires visibility mode (private, unlisted, public)" << RESET << endl; + exit(1); + } + ostringstream json; + json << "{\"visibility\":\"" << visibility << "\"}"; + string path = "/images/" + visibility_id + "/visibility"; + string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key); + string cmd = "curl -s -X POST '" + API_BASE + path + "' " + "-H 'Content-Type: application/json' " + + auth_headers + " -d '" + json.str() + "'"; + exec_curl(cmd); + cout << GREEN << "Image visibility set to: " << visibility << RESET << endl; + return; + } + + if (!spawn.empty()) { + ostringstream json; + json << "{"; + bool has_field = false; + if (!name.empty()) { + json << "\"name\":\"" << escape_json(name) << "\""; + has_field = true; + } + if (!ports.empty()) { + if (has_field) json << ","; + json << "\"ports\":[" << ports << "]"; + } + json << "}"; + string path = "/images/" + spawn + "/spawn"; + string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key); + string cmd = "curl -s -X POST '" + API_BASE + path + "' " + "-H 'Content-Type: application/json' " + + auth_headers + " -d '" + json.str() + "'"; + cout << GREEN << "Service spawned from image" << RESET << endl; + cout << exec_curl(cmd) << endl; + return; + } + + if (!clone.empty()) { + ostringstream json; + json << "{"; + if (!name.empty()) { + json << "\"name\":\"" << escape_json(name) << "\""; + } + json << "}"; + string path = "/images/" + clone + "/clone"; + string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key); + string cmd = "curl -s -X POST '" + API_BASE + path + "' " + "-H 'Content-Type: application/json' " + + auth_headers + " -d '" + json.str() + "'"; + cout << GREEN << "Image cloned" << RESET << endl; + cout << exec_curl(cmd) << endl; + return; + } + + // Default: list images + string auth_headers = build_auth_headers("GET", "/images", "", public_key, secret_key); + string cmd = "curl -s -X GET '" + API_BASE + "/images' " + auth_headers; + cout << exec_curl(cmd) << endl; +} + void cmd_validate_key(bool extend, const string& public_key, const string& secret_key) { string auth_headers = build_auth_headers("POST", "/keys/validate", "", public_key, secret_key); string cmd = "curl -s -X POST '" + PORTAL_BASE + "/keys/validate' " @@ -787,14 +962,45 @@ int main(int argc, char* argv[]) { if (argc < 2) { cerr << "Usage: " << argv[0] << " [options] " << endl; + cerr << " " << argv[0] << " languages [--json]" << endl; cerr << " " << argv[0] << " session [options]" << endl; cerr << " " << argv[0] << " service [options]" << endl; + cerr << " " << argv[0] << " image [options]" << endl; cerr << " " << argv[0] << " key [options]" << endl; return 1; } string cmd_type = argv[1]; + if (cmd_type == "image") { + bool list = false; + string info, del, lock, unlock, publish, source_type, visibility_id, visibility; + string spawn, clone, name, ports; + + for (int i = 2; i < argc; i++) { + string arg = argv[i]; + if (arg == "--list" || arg == "-l") list = true; + else if (arg == "--info" && i+1 < argc) info = argv[++i]; + else if (arg == "--delete" && i+1 < argc) del = argv[++i]; + else if (arg == "--lock" && i+1 < argc) lock = argv[++i]; + else if (arg == "--unlock" && i+1 < argc) unlock = argv[++i]; + else if (arg == "--publish" && i+1 < argc) publish = argv[++i]; + else if (arg == "--source-type" && i+1 < argc) source_type = argv[++i]; + else if (arg == "--visibility" && i+2 < argc) { + visibility_id = argv[++i]; + visibility = argv[++i]; + } + else if (arg == "--spawn" && i+1 < argc) spawn = argv[++i]; + else if (arg == "--clone" && i+1 < argc) clone = argv[++i]; + else if (arg == "--name" && i+1 < argc) name = argv[++i]; + else if (arg == "--ports" && i+1 < argc) ports = argv[++i]; + else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; + } + + cmd_image(list, info, del, lock, unlock, publish, source_type, visibility_id, visibility, spawn, clone, name, ports, public_key, secret_key); + return 0; + } + if (cmd_type == "session") { bool list = false; string kill, shell, network; @@ -881,6 +1087,19 @@ int main(int argc, char* argv[]) { return 0; } + if (cmd_type == "languages") { + bool json_output = false; + + for (int i = 2; i < argc; i++) { + string arg = argv[i]; + if (arg == "--json") json_output = true; + else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; + } + + cmd_languages(json_output, public_key, secret_key); + return 0; + } + // Execute mode vector envs, files; bool artifacts = false; diff --git a/clients/crystal/sync/src/un.cr b/clients/crystal/sync/src/un.cr index d206821..41f655a 100644 --- a/clients/crystal/sync/src/un.cr +++ b/clients/crystal/sync/src/un.cr @@ -414,6 +414,26 @@ def cmd_session(args) puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}" end +def cmd_languages(args) + public_key, secret_key = get_api_keys(args[:api_key]?) + + result = api_request("/languages", public_key, secret_key) + languages = result["languages"]?.try(&.as_a?) || [] of JSON::Any + + if args[:json]?.as?(Bool) + # Output as JSON array of language names + names = languages.map { |l| l["name"]?.try(&.as_s?) || "" }.reject(&.empty?) + puts names.to_json + else + # Output one language per line + languages.each do |lang| + if name = lang["name"]?.try(&.as_s?) + puts name + end + end + end +end + def cmd_key(args) public_key, secret_key = get_api_keys(args[:api_key]?) @@ -508,6 +528,100 @@ def cmd_key(args) end end +def cmd_image(args) + public_key, secret_key = get_api_keys(args[:api_key]?) + + if args[:list]?.as?(Bool) + result = api_request("/images", public_key, secret_key) + puts result.to_pretty_json + return + end + + if info_id = args[:image_info]?.as?(String) + result = api_request("/images/#{info_id}", public_key, secret_key) + puts result.to_pretty_json + return + end + + if del_id = args[:image_delete]?.as?(String) + api_request("/images/#{del_id}", public_key, secret_key, method: "DELETE") + puts "#{GREEN}Image deleted: #{del_id}#{RESET}" + return + end + + if lock_id = args[:image_lock]?.as?(String) + payload = JSON.parse({}.to_json) + api_request("/images/#{lock_id}/lock", public_key, secret_key, method: "POST", data: payload) + puts "#{GREEN}Image locked: #{lock_id}#{RESET}" + return + end + + if unlock_id = args[:image_unlock]?.as?(String) + payload = JSON.parse({}.to_json) + api_request("/images/#{unlock_id}/unlock", public_key, secret_key, method: "POST", data: payload) + puts "#{GREEN}Image unlocked: #{unlock_id}#{RESET}" + return + end + + if publish_id = args[:image_publish]?.as?(String) + source_type = args[:image_source_type]?.as?(String) + if source_type.nil? || source_type.empty? + STDERR.puts "#{RED}Error: --publish requires --source-type (service or snapshot)#{RESET}" + exit 1 + end + payload = JSON.parse({source_type: source_type, source_id: publish_id}.to_json) + if name = args[:image_name]?.as?(String) + payload.as_h["name"] = JSON::Any.new(name) + end + result = api_request("/images/publish", public_key, secret_key, method: "POST", data: payload) + puts "#{GREEN}Image published#{RESET}" + puts result.to_pretty_json + return + end + + if visibility_id = args[:image_visibility_id]?.as?(String) + visibility = args[:image_visibility]?.as?(String) + if visibility.nil? || visibility.empty? + STDERR.puts "#{RED}Error: --visibility requires visibility mode (private, unlisted, public)#{RESET}" + exit 1 + end + payload = JSON.parse({visibility: visibility}.to_json) + api_request("/images/#{visibility_id}/visibility", public_key, secret_key, method: "POST", data: payload) + puts "#{GREEN}Image visibility set to: #{visibility}#{RESET}" + return + end + + if spawn_id = args[:image_spawn]?.as?(String) + payload = JSON.parse({}.to_json) + if name = args[:image_name]?.as?(String) + payload.as_h["name"] = JSON::Any.new(name) + end + if ports_str = args[:image_ports]?.as?(String) + ports = ports_str.split(',').map(&.to_i) + payload.as_h["ports"] = JSON.parse(ports.to_json) + end + result = api_request("/images/#{spawn_id}/spawn", public_key, secret_key, method: "POST", data: payload) + puts "#{GREEN}Service spawned from image#{RESET}" + puts result.to_pretty_json + return + end + + if clone_id = args[:image_clone]?.as?(String) + payload = JSON.parse({}.to_json) + if name = args[:image_name]?.as?(String) + payload.as_h["name"] = JSON::Any.new(name) + end + result = api_request("/images/#{clone_id}/clone", public_key, secret_key, method: "POST", data: payload) + puts "#{GREEN}Image cloned#{RESET}" + puts result.to_pretty_json + return + end + + # Default: list images + result = api_request("/images", public_key, secret_key) + puts result.to_pretty_json +end + def cmd_service(args) # Handle env subcommand if env_action = args[:env_action]?.as?(String) @@ -735,11 +849,24 @@ def main svc_envs: [] of String, svc_env_file: nil, env_action: nil, - env_target: nil + env_target: nil, + json: false, + image_info: nil, + image_delete: nil, + image_lock: nil, + image_unlock: nil, + image_publish: nil, + image_source_type: nil, + image_visibility_id: nil, + image_visibility: nil, + image_spawn: nil, + image_clone: nil, + image_name: nil, + image_ports: nil } of Symbol => (String | Array(String) | Bool | Nil) parser = OptionParser.new do |opts| - opts.banner = "Usage: un.cr [options] \n un.cr session [options]\n un.cr service [options]\n un.cr service env [options]\n un.cr key [options]\n\nService env commands:\n env status Show vault status\n env set Set vault (-e KEY=VALUE or --env-file FILE)\n env export Export vault contents\n env delete Delete vault" + opts.banner = "Usage: un.cr [options] \n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env [options]\n un.cr key [options]\n\nService env commands:\n env status Show vault status\n env set Set vault (-e KEY=VALUE or --env-file FILE)\n env export Export vault contents\n env delete Delete vault" opts.on("-k API_KEY", "--api-key=API_KEY", "API key") { |k| args[:api_key] = k } opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n } @@ -771,6 +898,7 @@ def main opts.on("--bootstrap-file=FILE", "Upload local file as bootstrap script") { |f| args[:bootstrap_file] = f } opts.on("--env-file=FILE", "Load env vars from file (for vault)") { |f| args[:svc_env_file] = f } opts.on("--extend", "Open browser to extend/renew key") { args[:extend] = true } + opts.on("--json", "Output as JSON array (for languages command)") { args[:json] = true } opts.unknown_args do |before, after| if before.size > 0 @@ -800,6 +928,99 @@ def main end when "key" args[:command] = "key" + when "languages" + args[:command] = "languages" + when "image" + args[:command] = "image" + # Parse image subcommand options + i = 1 + while i < before.size + case before[i] + when "--list", "-l" + args[:list] = true + i += 1 + when "--info" + if i + 1 < before.size + args[:image_info] = before[i + 1] + i += 2 + else + i += 1 + end + when "--delete" + if i + 1 < before.size + args[:image_delete] = before[i + 1] + i += 2 + else + i += 1 + end + when "--lock" + if i + 1 < before.size + args[:image_lock] = before[i + 1] + i += 2 + else + i += 1 + end + when "--unlock" + if i + 1 < before.size + args[:image_unlock] = before[i + 1] + i += 2 + else + i += 1 + end + when "--publish" + if i + 1 < before.size + args[:image_publish] = before[i + 1] + i += 2 + else + i += 1 + end + when "--source-type" + if i + 1 < before.size + args[:image_source_type] = before[i + 1] + i += 2 + else + i += 1 + end + when "--visibility" + if i + 2 < before.size + args[:image_visibility_id] = before[i + 1] + args[:image_visibility] = before[i + 2] + i += 3 + else + i += 1 + end + when "--spawn" + if i + 1 < before.size + args[:image_spawn] = before[i + 1] + i += 2 + else + i += 1 + end + when "--clone" + if i + 1 < before.size + args[:image_clone] = before[i + 1] + i += 2 + else + i += 1 + end + when "--name" + if i + 1 < before.size + args[:image_name] = before[i + 1] + i += 2 + else + i += 1 + end + when "--ports" + if i + 1 < before.size + args[:image_ports] = before[i + 1] + i += 2 + else + i += 1 + end + else + i += 1 + end + end else if before[0].starts_with?("-") STDERR.puts "#{RED}Unknown option: #{before[0]}#{RESET}" @@ -820,6 +1041,10 @@ def main cmd_service(args) elsif args[:command] == "key" cmd_key(args) + elsif args[:command] == "languages" + cmd_languages(args) + elsif args[:command] == "image" + cmd_image(args) elsif args[:source_file] cmd_execute(args) else diff --git a/clients/d/sync/src/un.d b/clients/d/sync/src/un.d index bca7d34..eaf6f19 100644 --- a/clients/d/sync/src/un.d +++ b/clients/d/sync/src/un.d @@ -575,6 +575,162 @@ void cmdService(string name, string ports, string bootstrap, string bootstrapFil exit(1); } +void cmdImage(bool list, string info, string del, string lock, string unlock, + string publish, string sourceType, string visibilityId, string visibility, + string spawn, string clone, string name, string ports, string publicKey, string secretKey) { + if (list) { + string authHeaders = buildAuthHeaders("GET", "/images", "", publicKey, secretKey); + string cmd = format(`curl -s -X GET '%s/images' %s`, API_BASE, authHeaders); + writeln(execCurl(cmd)); + return; + } + + if (!info.empty) { + string path = format("/images/%s", info); + string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey); + string cmd = format(`curl -s -X GET '%s/images/%s' %s`, API_BASE, info, authHeaders); + writeln(execCurl(cmd)); + return; + } + + if (!del.empty) { + string path = format("/images/%s", del); + string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey); + string cmd = format(`curl -s -X DELETE '%s/images/%s' %s`, API_BASE, del, authHeaders); + execCurl(cmd); + writefln("%sImage deleted: %s%s", GREEN, del, RESET); + return; + } + + if (!lock.empty) { + string path = format("/images/%s/lock", lock); + string json = "{}"; + string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey); + string cmd = format(`curl -s -X POST '%s/images/%s/lock' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, lock, authHeaders, json); + execCurl(cmd); + writefln("%sImage locked: %s%s", GREEN, lock, RESET); + return; + } + + if (!unlock.empty) { + string path = format("/images/%s/unlock", unlock); + string json = "{}"; + string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey); + string cmd = format(`curl -s -X POST '%s/images/%s/unlock' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, unlock, authHeaders, json); + execCurl(cmd); + writefln("%sImage unlocked: %s%s", GREEN, unlock, RESET); + return; + } + + if (!publish.empty) { + if (sourceType.empty) { + stderr.writefln("%sError: --publish requires --source-type (service or snapshot)%s", RED, RESET); + exit(1); + } + string json = format(`{"source_type":"%s","source_id":"%s"`, sourceType, publish); + if (!name.empty) { + json ~= format(`,"name":"%s"`, escapeJson(name)); + } + json ~= "}"; + string path = "/images/publish"; + string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey); + string cmd = format(`curl -s -X POST '%s/images/publish' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json); + writefln("%sImage published%s", GREEN, RESET); + writeln(execCurl(cmd)); + return; + } + + if (!visibilityId.empty) { + if (visibility.empty) { + stderr.writefln("%sError: --visibility requires visibility mode (private, unlisted, public)%s", RED, RESET); + exit(1); + } + string json = format(`{"visibility":"%s"}`, visibility); + string path = format("/images/%s/visibility", visibilityId); + string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey); + string cmd = format(`curl -s -X POST '%s/images/%s/visibility' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, visibilityId, authHeaders, json); + execCurl(cmd); + writefln("%sImage visibility set to: %s%s", GREEN, visibility, RESET); + return; + } + + if (!spawn.empty) { + string json = "{"; + bool hasField = false; + if (!name.empty) { + json ~= format(`"name":"%s"`, escapeJson(name)); + hasField = true; + } + if (!ports.empty) { + if (hasField) json ~= ","; + json ~= format(`"ports":[%s]`, ports); + } + json ~= "}"; + string path = format("/images/%s/spawn", spawn); + string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey); + string cmd = format(`curl -s -X POST '%s/images/%s/spawn' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, spawn, authHeaders, json); + writefln("%sService spawned from image%s", GREEN, RESET); + writeln(execCurl(cmd)); + return; + } + + if (!clone.empty) { + string json = "{"; + if (!name.empty) { + json ~= format(`"name":"%s"`, escapeJson(name)); + } + json ~= "}"; + string path = format("/images/%s/clone", clone); + string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey); + string cmd = format(`curl -s -X POST '%s/images/%s/clone' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, clone, authHeaders, json); + writefln("%sImage cloned%s", GREEN, RESET); + writeln(execCurl(cmd)); + return; + } + + // Default: list images + string authHeaders = buildAuthHeaders("GET", "/images", "", publicKey, secretKey); + string cmd = format(`curl -s -X GET '%s/images' %s`, API_BASE, authHeaders); + writeln(execCurl(cmd)); +} + +void cmdLanguages(bool jsonOutput, string publicKey, string secretKey) { + string authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey); + string cmd = format(`curl -s -X GET '%s/languages' %s`, API_BASE, authHeaders); + string result = execCurl(cmd); + + if (jsonOutput) { + // Extract language names and output as JSON array + string[] names; + import std.algorithm : findSplitAfter; + string remaining = result; + while (true) { + auto search = remaining.findSplitAfter(`"name":"`); + if (search[0].length == 0) break; + auto endSearch = search[1].findSplitAfter(`"`); + if (endSearch[0].length > 1) { + names ~= endSearch[0][0..$-1]; + } + remaining = endSearch[1]; + } + import std.array : join; + writefln("[%s]", names.map!(n => format(`"%s"`, n)).join(",")); + } else { + // Output one language per line + import std.algorithm : findSplitAfter; + string remaining = result; + while (true) { + auto search = remaining.findSplitAfter(`"name":"`); + if (search[0].length == 0) break; + auto endSearch = search[1].findSplitAfter(`"`); + if (endSearch[0].length > 1) { + writeln(endSearch[0][0..$-1]); + } + remaining = endSearch[1]; + } + } +} + void openBrowser(string url) { version(linux) { executeShell("xdg-open \"" ~ url ~ "\" 2>/dev/null &"); @@ -707,9 +863,11 @@ int main(string[] args) { if (args.length < 2) { stderr.writefln("Usage: %s [options] ", args[0]); + stderr.writefln(" %s languages [--json]", args[0]); stderr.writefln(" %s session [options]", args[0]); stderr.writefln(" %s service [options]", args[0]); stderr.writefln(" %s service env [options]", args[0]); + stderr.writefln(" %s image [options]", args[0]); stderr.writefln(" %s key [options]", args[0]); stderr.writeln(""); stderr.writeln("Service env commands:"); @@ -815,6 +973,56 @@ int main(string[] args) { return 0; } + if (args[1] == "languages") { + bool jsonOutput = false; + + for (size_t i = 2; i < args.length; i++) { + if (args[i] == "--json") jsonOutput = true; + else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; + } + + if (publicKey.empty) { + stderr.writefln("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s", RED, RESET); + return 1; + } + + cmdLanguages(jsonOutput, publicKey, secretKey); + return 0; + } + + if (args[1] == "image") { + bool list = false; + string info, del, lock, unlock, publish, sourceType, visibilityId, visibility; + string spawn, clone, name, ports; + + for (size_t i = 2; i < args.length; i++) { + if (args[i] == "--list" || args[i] == "-l") list = true; + else if (args[i] == "--info" && i+1 < args.length) info = args[++i]; + else if (args[i] == "--delete" && i+1 < args.length) del = args[++i]; + else if (args[i] == "--lock" && i+1 < args.length) lock = args[++i]; + else if (args[i] == "--unlock" && i+1 < args.length) unlock = args[++i]; + else if (args[i] == "--publish" && i+1 < args.length) publish = args[++i]; + else if (args[i] == "--source-type" && i+1 < args.length) sourceType = args[++i]; + else if (args[i] == "--visibility" && i+2 < args.length) { + visibilityId = args[++i]; + visibility = args[++i]; + } + else if (args[i] == "--spawn" && i+1 < args.length) spawn = args[++i]; + else if (args[i] == "--clone" && i+1 < args.length) clone = args[++i]; + else if (args[i] == "--name" && i+1 < args.length) name = args[++i]; + else if (args[i] == "--ports" && i+1 < args.length) ports = args[++i]; + else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; + } + + if (publicKey.empty) { + stderr.writefln("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s", RED, RESET); + return 1; + } + + cmdImage(list, info, del, lock, unlock, publish, sourceType, visibilityId, visibility, spawn, clone, name, ports, publicKey, secretKey); + return 0; + } + // Execute mode string[] envs; bool artifacts = false; diff --git a/clients/dart/sync/src/un.dart b/clients/dart/sync/src/un.dart index a5e7be4..367e1b6 100644 --- a/clients/dart/sync/src/un.dart +++ b/clients/dart/sync/src/un.dart @@ -82,6 +82,7 @@ class Args { String? sessionShell; String? sessionKill; bool serviceList = false; + bool languagesJson = false; String? serviceName; String? servicePorts; String? serviceType; @@ -103,6 +104,20 @@ class Args { String? envFile; String? envAction; String? envTarget; + // Image command options + bool imageList = false; + String? imageInfo; + String? imageDelete; + String? imageLock; + String? imageUnlock; + String? imagePublish; + String? imageSourceType; + String? imageVisibility; + String? imageVisibilityMode; + String? imageSpawn; + String? imageClone; + String? imageName; + String? imagePorts; } List getApiKeys(String? argsKey) { @@ -702,6 +717,118 @@ Future cmdService(Args args) async { exit(1); } +Future cmdLanguages(Args args) async { + final keys = getApiKeys(args.apiKey); + final publicKey = keys[0]!; + final secretKey = keys[1]; + + final result = await apiRequestCurl('/languages', 'GET', null, publicKey, secretKey); + final languages = result['languages'] as List? ?? []; + + if (args.languagesJson) { + // JSON output: print as array + print(jsonEncode(languages)); + } else { + // Default: one language per line + for (final lang in languages) { + print(lang.toString()); + } + } +} + +Future cmdImage(Args args) async { + final keys = getApiKeys(args.apiKey); + final publicKey = keys[0]!; + final secretKey = keys[1]; + + if (args.imageList) { + final result = await apiRequestCurl('/images', 'GET', null, publicKey, secretKey); + print(jsonEncode(result)); + return; + } + + if (args.imageInfo != null) { + final result = await apiRequestCurl('/images/${args.imageInfo}', 'GET', null, publicKey, secretKey); + print(jsonEncode(result)); + return; + } + + if (args.imageDelete != null) { + await apiRequestCurl('/images/${args.imageDelete}', 'DELETE', null, publicKey, secretKey); + print('${green}Image deleted: ${args.imageDelete}$reset'); + return; + } + + if (args.imageLock != null) { + await apiRequestCurl('/images/${args.imageLock}/lock', 'POST', null, publicKey, secretKey); + print('${green}Image locked: ${args.imageLock}$reset'); + return; + } + + if (args.imageUnlock != null) { + await apiRequestCurl('/images/${args.imageUnlock}/unlock', 'POST', null, publicKey, secretKey); + print('${green}Image unlocked: ${args.imageUnlock}$reset'); + return; + } + + if (args.imagePublish != null) { + if (args.imageSourceType == null) { + stderr.writeln('${red}Error: --source-type required (service or snapshot)$reset'); + exit(1); + } + final payload = { + 'source_type': args.imageSourceType, + 'source_id': args.imagePublish, + }; + if (args.imageName != null) { + payload['name'] = args.imageName; + } + final result = await apiRequestCurl('/images/publish', 'POST', jsonEncode(payload), publicKey, secretKey); + print('${green}Image published$reset'); + print(jsonEncode(result)); + return; + } + + if (args.imageVisibility != null) { + if (args.imageVisibilityMode == null) { + stderr.writeln('${red}Error: --visibility requires MODE (private, unlisted, or public)$reset'); + exit(1); + } + final payload = {'visibility': args.imageVisibilityMode}; + await apiRequestCurl('/images/${args.imageVisibility}/visibility', 'POST', jsonEncode(payload), publicKey, secretKey); + print('${green}Image visibility set to ${args.imageVisibilityMode}$reset'); + return; + } + + if (args.imageSpawn != null) { + final payload = {}; + if (args.imageName != null) { + payload['name'] = args.imageName; + } + if (args.imagePorts != null) { + payload['ports'] = args.imagePorts!.split(',').map((p) => int.parse(p.trim())).toList(); + } + final result = await apiRequestCurl('/images/${args.imageSpawn}/spawn', 'POST', jsonEncode(payload), publicKey, secretKey); + print('${green}Service spawned from image$reset'); + print(jsonEncode(result)); + return; + } + + if (args.imageClone != null) { + final payload = {}; + if (args.imageName != null) { + payload['name'] = args.imageName; + } + final result = await apiRequestCurl('/images/${args.imageClone}/clone', 'POST', jsonEncode(payload), publicKey, secretKey); + print('${green}Image cloned$reset'); + print(jsonEncode(result)); + return; + } + + stderr.writeln('${red}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID$reset'); + exit(1); +} + Future cmdKey(Args args) async { final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; @@ -771,9 +898,20 @@ Args parseArgs(List argv) { case 'service': args.command = 'service'; break; + case 'image': + args.command = 'image'; + break; case 'key': args.command = 'key'; break; + case 'languages': + args.command = 'languages'; + break; + case '--json': + if (args.command == 'languages') { + args.languagesJson = true; + } + break; case '-k': case '--api-key': args.apiKey = argv[++i]; @@ -808,6 +946,8 @@ Args parseArgs(List argv) { args.sessionList = true; } else if (args.command == 'service') { args.serviceList = true; + } else if (args.command == 'image') { + args.imageList = true; } break; case '-s': @@ -818,10 +958,18 @@ Args parseArgs(List argv) { args.sessionKill = argv[++i]; break; case '--name': - args.serviceName = argv[++i]; + if (args.command == 'image') { + args.imageName = argv[++i]; + } else { + args.serviceName = argv[++i]; + } break; case '--ports': - args.servicePorts = argv[++i]; + if (args.command == 'image') { + args.imagePorts = argv[++i]; + } else { + args.servicePorts = argv[++i]; + } break; case '--type': args.serviceType = argv[++i]; @@ -832,9 +980,7 @@ Args parseArgs(List argv) { case '--bootstrap-file': args.serviceBootstrapFile = argv[++i]; break; - case '--info': - args.serviceInfo = argv[++i]; - break; + // --info handled below in the image command section case '--logs': args.serviceLogs = argv[++i]; break; @@ -874,6 +1020,54 @@ Args parseArgs(List argv) { case '--env-file': args.envFile = argv[++i]; break; + case '--info': + if (args.command == 'service') { + args.serviceInfo = argv[++i]; + } else if (args.command == 'image') { + args.imageInfo = argv[++i]; + } + break; + case '--delete': + if (args.command == 'image') { + args.imageDelete = argv[++i]; + } + break; + case '--lock': + if (args.command == 'image') { + args.imageLock = argv[++i]; + } + break; + case '--unlock': + if (args.command == 'image') { + args.imageUnlock = argv[++i]; + } + break; + case '--publish': + if (args.command == 'image') { + args.imagePublish = argv[++i]; + } + break; + case '--source-type': + args.imageSourceType = argv[++i]; + break; + case '--visibility': + if (args.command == 'image') { + args.imageVisibility = argv[++i]; + if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) { + args.imageVisibilityMode = argv[++i]; + } + } + break; + case '--spawn': + if (args.command == 'image') { + args.imageSpawn = argv[++i]; + } + break; + case '--clone': + if (args.command == 'image') { + args.imageClone = argv[++i]; + } + break; case 'env': if (args.command == 'service' && i + 1 < argv.length) { args.envAction = argv[++i]; @@ -900,7 +1094,9 @@ void printHelp() { Usage: dart un.dart [options] dart un.dart session [options] dart un.dart service [options] + dart un.dart image [options] dart un.dart key [options] + dart un.dart languages [--json] Execute options: -e KEY=VALUE Set environment variable @@ -941,8 +1137,25 @@ Service env commands: env export ID Export vault contents env delete ID Delete vault +Image options: + -l, --list List all images + --info ID Get image details + --delete ID Delete an image + --lock ID Lock image to prevent deletion + --unlock ID Unlock image + --publish ID Publish image from service/snapshot (requires --source-type) + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility: private, unlisted, or public + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + Key options: --extend Open browser to extend key + +Languages options: + --json Output as JSON array '''); } @@ -954,8 +1167,12 @@ void main(List arguments) async { await cmdSession(args); } else if (args.command == 'service') { await cmdService(args); + } else if (args.command == 'image') { + await cmdImage(args); } else if (args.command == 'key') { await cmdKey(args); + } else if (args.command == 'languages') { + await cmdLanguages(args); } else if (args.sourceFile != null) { await cmdExecute(args); } else { diff --git a/clients/elixir/sync/src/un.ex b/clients/elixir/sync/src/un.ex index f68dddd..0092d3f 100755 --- a/clients/elixir/sync/src/un.ex +++ b/clients/elixir/sync/src/un.ex @@ -79,7 +79,9 @@ defmodule Un do def main(["session" | rest]), do: session_command(rest) def main(["service" | rest]), do: service_command(rest) def main(["snapshot" | rest]), do: snapshot_command(rest) + def main(["image" | rest]), do: image_command(rest) def main(["key" | rest]), do: key_command(rest) + def main(["languages" | rest]), do: languages_command(rest) def main(args), do: execute_command(args) defp print_usage do @@ -88,10 +90,16 @@ defmodule Un do IO.puts(" un.ex service [options]") IO.puts(" un.ex service env ") IO.puts(" un.ex snapshot [options]") + IO.puts(" un.ex image [options]") IO.puts(" un.ex key [--extend]") + IO.puts(" un.ex languages [--json]") IO.puts("") IO.puts("Service options: --name, --ports, --bootstrap, -e KEY=VALUE, --env-file FILE") IO.puts("Service env commands: status, set, export, delete") + IO.puts("Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,") + IO.puts(" --publish ID --source-type TYPE, --visibility ID MODE,") + IO.puts(" --spawn ID, --clone ID, --name NAME, --ports PORTS") + IO.puts("Languages options: --json (output as JSON array)") System.halt(1) end @@ -475,6 +483,117 @@ defmodule Un do System.halt(1) end + # Image command + defp image_command(["--list" | _]) do + image_command(["-l"]) + end + + defp image_command(["-l" | _]) do + api_key = get_api_key() + response = curl_get(api_key, "/images") + IO.puts(response) + end + + defp image_command(["--info", image_id | _]) do + api_key = get_api_key() + response = curl_get(api_key, "/images/#{image_id}") + IO.puts(response) + end + + defp image_command(["--delete", image_id | _]) do + api_key = get_api_key() + curl_delete(api_key, "/images/#{image_id}") + IO.puts("#{@green}Image deleted: #{image_id}#{@reset}") + end + + defp image_command(["--lock", image_id | _]) do + api_key = get_api_key() + curl_post(api_key, "/images/#{image_id}/lock", "{}") + IO.puts("#{@green}Image locked: #{image_id}#{@reset}") + end + + defp image_command(["--unlock", image_id | _]) do + api_key = get_api_key() + curl_post(api_key, "/images/#{image_id}/unlock", "{}") + IO.puts("#{@green}Image unlocked: #{image_id}#{@reset}") + end + + defp image_command(["--publish", source_id | rest]) do + source_type = get_opt(rest, "--source-type", nil, nil) + if is_nil(source_type) do + IO.puts(:stderr, "#{@red}Error: --source-type required (service or snapshot)#{@reset}") + System.halt(1) + end + api_key = get_api_key() + name = get_opt(rest, "--name", nil, nil) + name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: "" + json = "{\"source_type\":\"#{source_type}\",\"source_id\":\"#{source_id}\"#{name_json}}" + response = curl_post(api_key, "/images/publish", json) + IO.puts("#{@green}Image published#{@reset}") + IO.puts(response) + end + + defp image_command(["--visibility", image_id, mode | _]) do + api_key = get_api_key() + json = "{\"visibility\":\"#{mode}\"}" + curl_post(api_key, "/images/#{image_id}/visibility", json) + IO.puts("#{@green}Image visibility set to #{mode}#{@reset}") + end + + defp image_command(["--spawn", image_id | rest]) do + api_key = get_api_key() + name = get_opt(rest, "--name", nil, nil) + ports = get_opt(rest, "--ports", nil, nil) + name_json = if name, do: "\"name\":\"#{escape_json(name)}\"", else: "" + ports_json = if ports, do: "\"ports\":[#{ports}]", else: "" + parts = [name_json, ports_json] |> Enum.filter(&(&1 != "")) |> Enum.join(",") + json = "{#{parts}}" + response = curl_post(api_key, "/images/#{image_id}/spawn", json) + IO.puts("#{@green}Service spawned from image#{@reset}") + IO.puts(response) + end + + defp image_command(["--clone", image_id | rest]) do + api_key = get_api_key() + name = get_opt(rest, "--name", nil, nil) + json = if name, do: "{\"name\":\"#{escape_json(name)}\"}", else: "{}" + response = curl_post(api_key, "/images/#{image_id}/clone", json) + IO.puts("#{@green}Image cloned#{@reset}") + IO.puts(response) + end + + defp image_command(_) do + IO.puts(:stderr, "Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID") + System.halt(1) + end + + # Languages command + defp languages_command(args) do + api_key = get_api_key() + json_output = "--json" in args + + response = curl_get(api_key, "/languages") + languages = extract_json_array(response, "languages") + + if json_output do + # Output as JSON array + json_str = "[" <> Enum.map_join(languages, ",", &("\"#{&1}\"")) <> "]" + IO.puts(json_str) + else + # Output one language per line + Enum.each(languages, &IO.puts/1) + end + end + + defp extract_json_array(json_str, key) do + case Regex.run(~r/"#{key}"\s*:\s*\[([^\]]*)\]/, json_str) do + [_, array_content] -> + Regex.scan(~r/"([^"]*)"/, array_content) + |> Enum.map(fn [_, val] -> val end) + _ -> [] + end + end + # Key command defp key_command(args) do api_key = get_api_key() diff --git a/clients/erlang/sync/src/un.erl b/clients/erlang/sync/src/un.erl index 8a22a6e..5202321 100755 --- a/clients/erlang/sync/src/un.erl +++ b/clients/erlang/sync/src/un.erl @@ -47,7 +47,9 @@ main([]) -> io:format(" un.erl session [options]~n"), io:format(" un.erl service [options]~n"), io:format(" un.erl snapshot [options]~n"), + io:format(" un.erl image [options]~n"), io:format(" un.erl key [options]~n"), + io:format(" un.erl languages [--json]~n"), halt(1); main(["session" | Rest]) -> @@ -59,9 +61,15 @@ main(["service" | Rest]) -> main(["snapshot" | Rest]) -> snapshot_command(Rest); +main(["image" | Rest]) -> + image_command(Rest); + main(["key" | Rest]) -> key_command(Rest); +main(["languages" | Rest]) -> + languages_command(Rest); + main(Args) -> execute_command(Args). @@ -439,6 +447,140 @@ build_clone_json(Type, Name, Shell, Ports) -> end, TypeJson ++ NameJson ++ ShellJson ++ PortsJson ++ "}". +%% Image command +image_command(["--list" | _]) -> + image_command(["-l"]); +image_command(["-l" | _]) -> + {PublicKey, SecretKey} = get_api_keys(), + Result = api_request("/images", "GET", "", PublicKey, SecretKey), + io:format("~s~n", [Result]), + halt(0); + +image_command(["--info", ImageId | _]) -> + {PublicKey, SecretKey} = get_api_keys(), + Result = api_request("/images/" ++ ImageId, "GET", "", PublicKey, SecretKey), + io:format("~s~n", [Result]), + halt(0); + +image_command(["--delete", ImageId | _]) -> + {PublicKey, SecretKey} = get_api_keys(), + api_request("/images/" ++ ImageId, "DELETE", "", PublicKey, SecretKey), + io:format("\033[32mImage deleted: ~s\033[0m~n", [ImageId]), + halt(0); + +image_command(["--lock", ImageId | _]) -> + {PublicKey, SecretKey} = get_api_keys(), + api_request("/images/" ++ ImageId ++ "/lock", "POST", "", PublicKey, SecretKey), + io:format("\033[32mImage locked: ~s\033[0m~n", [ImageId]), + halt(0); + +image_command(["--unlock", ImageId | _]) -> + {PublicKey, SecretKey} = get_api_keys(), + api_request("/images/" ++ ImageId ++ "/unlock", "POST", "", PublicKey, SecretKey), + io:format("\033[32mImage unlocked: ~s\033[0m~n", [ImageId]), + halt(0); + +image_command(["--publish", SourceId | Rest]) -> + SourceType = get_image_source_type(Rest), + case SourceType of + undefined -> + io:format(standard_error, "\033[31mError: --source-type required (service or snapshot)\033[0m~n", []), + halt(1); + _ -> + {PublicKey, SecretKey} = get_api_keys(), + Name = get_image_name(Rest), + Payload = build_publish_json(SourceType, SourceId, Name), + Result = api_request("/images/publish", "POST", Payload, PublicKey, SecretKey), + io:format("\033[32mImage published\033[0m~n"), + io:format("~s~n", [Result]), + halt(0) + end; + +image_command(["--visibility", ImageId, Mode | _]) -> + {PublicKey, SecretKey} = get_api_keys(), + Payload = "{\"visibility\":\"" ++ Mode ++ "\"}", + api_request("/images/" ++ ImageId ++ "/visibility", "POST", Payload, PublicKey, SecretKey), + io:format("\033[32mImage visibility set to ~s\033[0m~n", [Mode]), + halt(0); + +image_command(["--spawn", ImageId | Rest]) -> + {PublicKey, SecretKey} = get_api_keys(), + Name = get_image_name(Rest), + Ports = get_image_ports(Rest), + Payload = build_spawn_json(Name, Ports), + Result = api_request("/images/" ++ ImageId ++ "/spawn", "POST", Payload, PublicKey, SecretKey), + io:format("\033[32mService spawned from image\033[0m~n"), + io:format("~s~n", [Result]), + halt(0); + +image_command(["--clone", ImageId | Rest]) -> + {PublicKey, SecretKey} = get_api_keys(), + Name = get_image_name(Rest), + Payload = case Name of + undefined -> "{}"; + N -> "{\"name\":\"" ++ escape_json(N) ++ "\"}" + end, + Result = api_request("/images/" ++ ImageId ++ "/clone", "POST", Payload, PublicKey, SecretKey), + io:format("\033[32mImage cloned\033[0m~n"), + io:format("~s~n", [Result]), + halt(0); + +image_command(_) -> + io:format(standard_error, "Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID~n", []), + halt(1). + +get_image_source_type([]) -> undefined; +get_image_source_type(["--source-type", Type | _]) -> Type; +get_image_source_type([_ | Rest]) -> get_image_source_type(Rest). + +get_image_name([]) -> undefined; +get_image_name(["--name", Name | _]) -> Name; +get_image_name([_ | Rest]) -> get_image_name(Rest). + +get_image_ports([]) -> undefined; +get_image_ports(["--ports", Ports | _]) -> Ports; +get_image_ports([_ | Rest]) -> get_image_ports(Rest). + +build_publish_json(SourceType, SourceId, Name) -> + Base = "{\"source_type\":\"" ++ SourceType ++ "\",\"source_id\":\"" ++ SourceId ++ "\"", + NameJson = case Name of + undefined -> ""; + N -> ",\"name\":\"" ++ escape_json(N) ++ "\"" + end, + Base ++ NameJson ++ "}". + +build_spawn_json(Name, Ports) -> + Parts = [], + Parts1 = case Name of + undefined -> Parts; + N -> ["\"name\":\"" ++ escape_json(N) ++ "\"" | Parts] + end, + Parts2 = case Ports of + undefined -> Parts1; + P -> ["\"ports\":[" ++ P ++ "]" | Parts1] + end, + "{" ++ string:join(lists:reverse(Parts2), ",") ++ "}". + +%% Languages command +languages_command(Args) -> + ApiKey = get_api_key(), + Response = curl_get(ApiKey, "/languages"), + JsonOutput = lists:member("--json", Args), + if + JsonOutput -> + %% Output raw JSON array + case extract_json_array(Response, "languages") of + [] -> io:format("[]~n"); + Languages -> io:format("[~s]~n", [string:join(["\"" ++ L ++ "\"" || L <- Languages], ",")]) + end; + true -> + %% Output one language per line + case extract_json_array(Response, "languages") of + [] -> ok; + Languages -> [io:format("~s~n", [L]) || L <- Languages] + end + end. + %% Key command key_command(Args) -> ApiKey = get_api_key(), @@ -857,3 +999,36 @@ extract_until_quote([$\\, $\" | Rest], Acc) -> extract_until_quote(Rest, [$\" | Acc]); extract_until_quote([C | Rest], Acc) -> extract_until_quote(Rest, [C | Acc]). + +%% Extract JSON array of strings from a field like "languages":["python","javascript",...] +extract_json_array(Json, Field) -> + Pattern = "\"" ++ Field ++ "\":[", + case string:str(Json, Pattern) of + 0 -> []; + Pos -> + Start = Pos + length(Pattern), + Rest = lists:nthtail(Start - 1, Json), + extract_array_elements(Rest, []) + end. + +extract_array_elements([], Acc) -> + lists:reverse(Acc); +extract_array_elements([$] | _], Acc) -> + lists:reverse(Acc); +extract_array_elements([$\" | Rest], Acc) -> + {Elem, Remainder} = extract_until_quote_with_rest(Rest), + extract_array_elements(Remainder, [Elem | Acc]); +extract_array_elements([_ | Rest], Acc) -> + extract_array_elements(Rest, Acc). + +extract_until_quote_with_rest(Str) -> + extract_until_quote_with_rest(Str, []). + +extract_until_quote_with_rest([], Acc) -> + {lists:reverse(Acc), []}; +extract_until_quote_with_rest([$\" | Rest], Acc) -> + {lists:reverse(Acc), Rest}; +extract_until_quote_with_rest([$\\, $\" | Rest], Acc) -> + extract_until_quote_with_rest(Rest, [$\" | Acc]); +extract_until_quote_with_rest([C | Rest], Acc) -> + extract_until_quote_with_rest(Rest, [C | Acc]). diff --git a/clients/forth/sync/src/un.forth b/clients/forth/sync/src/un.forth index a3fe5fe..7eab014 100644 --- a/clients/forth/sync/src/un.forth +++ b/clients/forth/sync/src/un.forth @@ -986,6 +986,395 @@ 1 (bye) ; +\ Languages list +: languages-list ( json-flag -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:GET:/languages:\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + if + \ JSON output + s" curl -s -X GET https://api.unsandbox.com/languages -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -c '.languages'" r@ write-line throw + else + \ One per line + s" curl -s -X GET https://api.unsandbox.com/languages -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.languages[]'" r@ write-line throw + then + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Handle languages subcommand +: handle-languages ( -- ) + argc @ 3 < if + 0 languages-list + 0 (bye) + then + + 2 arg 2dup s" --json" compare 0= if + 2drop + 1 languages-list + 0 (bye) + then + + 2drop + 0 languages-list + 0 (bye) +; + +\ Image list +: image-list ( -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:GET:/images:\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X GET https://api.unsandbox.com/images -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image info +: image-info ( addr len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:GET:/images/$IMAGE_ID:\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X GET https://api.unsandbox.com/images/$IMAGE_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image delete +: image-delete ( addr len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:DELETE:/images/$IMAGE_ID:\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X DELETE https://api.unsandbox.com/images/$IMAGE_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mImage deleted: " r@ write-file throw + r@ write-file throw + s" \\x1b[0m'" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image lock +: image-lock ( addr len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/lock:\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/lock -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mImage locked: " r@ write-file throw + r@ write-file throw + s" \\x1b[0m'" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image unlock +: image-unlock ( addr len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/unlock:\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/unlock -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mImage unlocked: " r@ write-file throw + r@ write-file throw + s" \\x1b[0m'" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image visibility +: image-visibility ( image-id-addr image-id-len mode-addr mode-len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2over r@ write-file throw + s" '" r@ write-line throw + s" MODE='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" BODY='{\"visibility\":\"'$MODE'\"}'" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/visibility:$BODY\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/visibility -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" >/dev/null && echo -e \"\\x1b[32mImage visibility set to $MODE\\x1b[0m\"" r@ write-line throw + r> close-file throw + 2drop 2drop \ clean up the stack + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image spawn +: image-spawn ( addr len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" NAME=''; PORTS=''" r@ write-line throw + s" i=4" r@ write-line throw + s" while [ $i -le $# ]; do" r@ write-line throw + s" arg=${!i}" r@ write-line throw + s" case \"$arg\" in" r@ write-line throw + s" --name) ((i++)); NAME=${!i} ;;" r@ write-line throw + s" --ports) ((i++)); PORTS=${!i} ;;" r@ write-line throw + s" esac" r@ write-line throw + s" ((i++))" r@ write-line throw + s" done" r@ write-line throw + s" BODY='{}'" r@ write-line throw + s" [ -n \"$NAME\" ] && BODY=$(echo $BODY | jq --arg n \"$NAME\" '. + {name: $n}')" r@ write-line throw + s" [ -n \"$PORTS\" ] && BODY=$(echo $BODY | jq --arg p \"$PORTS\" '. + {ports: ($p | split(\",\") | map(tonumber))}')" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/spawn:$BODY\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/spawn -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" | jq ." r@ write-line throw + s" echo -e '\\x1b[32mService spawned from image\\x1b[0m'" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh \"$@\" && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image clone +: image-clone ( addr len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" IMAGE_ID='" r@ write-file throw + 2dup r@ write-file throw + s" '" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" NAME=''" r@ write-line throw + s" i=4" r@ write-line throw + s" while [ $i -le $# ]; do" r@ write-line throw + s" arg=${!i}" r@ write-line throw + s" case \"$arg\" in" r@ write-line throw + s" --name) ((i++)); NAME=${!i} ;;" r@ write-line throw + s" esac" r@ write-line throw + s" ((i++))" r@ write-line throw + s" done" r@ write-line throw + s" BODY='{}'" r@ write-line throw + s" [ -n \"$NAME\" ] && BODY=$(echo $BODY | jq --arg n \"$NAME\" '. + {name: $n}')" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/clone:$BODY\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/clone -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" | jq ." r@ write-line throw + s" echo -e '\\x1b[32mImage cloned\\x1b[0m'" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh \"$@\" && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Image publish +: image-publish ( -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" PUBLIC_KEY='" r@ write-file throw + get-public-key r@ write-file throw + s" '" r@ write-line throw + s" SECRET_KEY='" r@ write-file throw + get-secret-key r@ write-file throw + s" '" r@ write-line throw + s" SOURCE_ID=''; SOURCE_TYPE=''; NAME=''" r@ write-line throw + s" i=3" r@ write-line throw + s" while [ $i -le $# ]; do" r@ write-line throw + s" arg=${!i}" r@ write-line throw + s" case \"$arg\" in" r@ write-line throw + s" --publish) ((i++)); SOURCE_ID=${!i} ;;" r@ write-line throw + s" --source-type) ((i++)); SOURCE_TYPE=${!i} ;;" r@ write-line throw + s" --name) ((i++)); NAME=${!i} ;;" r@ write-line throw + s" esac" r@ write-line throw + s" ((i++))" r@ write-line throw + s" done" r@ write-line throw + s" [ -z \"$SOURCE_TYPE\" ] && echo -e '\\x1b[31mError: --source-type required (service or snapshot)\\x1b[0m' >&2 && exit 1" r@ write-line throw + s" BODY='{\"source_type\":\"'$SOURCE_TYPE'\",\"source_id\":\"'$SOURCE_ID'\"}'" r@ write-line throw + s" [ -n \"$NAME\" ] && BODY=$(echo $BODY | jq --arg n \"$NAME\" '. + {name: $n}')" r@ write-line throw + s" TIMESTAMP=$(date +%s)" r@ write-line throw + s" MESSAGE=\"$TIMESTAMP:POST:/images/publish:$BODY\"" r@ write-line throw + s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/images/publish -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" | jq ." r@ write-line throw + s" echo -e '\\x1b[32mImage published\\x1b[0m'" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh \"$@\" && rm -f /tmp/unsandbox_cmd.sh" system +; + +\ Handle image subcommand +: handle-image ( -- ) + argc @ 3 < if + s" Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID" type cr + 1 (bye) + then + + 2 arg 2dup s" --list" compare 0= if + 2drop image-list + 0 (bye) + then + + 2dup s" -l" compare 0= if + 2drop image-list + 0 (bye) + then + + 2dup s" --info" compare 0= if + 2drop + argc @ 4 < if + s" Error: --info requires image ID" type cr + 1 (bye) + then + 3 arg image-info + 0 (bye) + then + + 2dup s" --delete" compare 0= if + 2drop + argc @ 4 < if + s" Error: --delete requires image ID" type cr + 1 (bye) + then + 3 arg image-delete + 0 (bye) + then + + 2dup s" --lock" compare 0= if + 2drop + argc @ 4 < if + s" Error: --lock requires image ID" type cr + 1 (bye) + then + 3 arg image-lock + 0 (bye) + then + + 2dup s" --unlock" compare 0= if + 2drop + argc @ 4 < if + s" Error: --unlock requires image ID" type cr + 1 (bye) + then + 3 arg image-unlock + 0 (bye) + then + + 2dup s" --publish" compare 0= if + 2drop + argc @ 4 < if + s" Error: --publish requires source ID" type cr + 1 (bye) + then + image-publish + 0 (bye) + then + + 2dup s" --visibility" compare 0= if + 2drop + argc @ 5 < if + s" Error: --visibility requires image ID and mode" type cr + 1 (bye) + then + 3 arg 4 arg image-visibility + 0 (bye) + then + + 2dup s" --spawn" compare 0= if + 2drop + argc @ 4 < if + s" Error: --spawn requires image ID" type cr + 1 (bye) + then + 3 arg image-spawn + 0 (bye) + then + + 2dup s" --clone" compare 0= if + 2drop + argc @ 4 < if + s" Error: --clone requires image ID" type cr + 1 (bye) + then + 3 arg image-clone + 0 (bye) + then + + 2drop + s" Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID" type cr + 1 (bye) +; + \ Main program : main \ Get command line argument count @@ -993,7 +1382,9 @@ s" Usage: gforth un.forth " type cr s" gforth un.forth session [options]" type cr s" gforth un.forth service [options]" type cr + s" gforth un.forth image [options]" type cr s" gforth un.forth key [options]" type cr + s" gforth un.forth languages [--json]" type cr 1 (bye) then @@ -1011,11 +1402,21 @@ 0 (bye) then + 2dup s" image" compare 0= if + 2drop handle-image + 0 (bye) + then + 2dup s" key" compare 0= if 2drop handle-key 0 (bye) then + 2dup s" languages" compare 0= if + 2drop handle-languages + 0 (bye) + then + \ Default: execute file execute-file ; diff --git a/clients/fortran/sync/src/un.f90 b/clients/fortran/sync/src/un.f90 index 8aa00ab..11b3b33 100644 --- a/clients/fortran/sync/src/un.f90 +++ b/clients/fortran/sync/src/un.f90 @@ -884,6 +884,12 @@ program unsandbox_cli is_key = .true. call handle_key() stop 0 + else if (trim(arg) == 'languages') then + call handle_languages() + stop 0 + else if (trim(arg) == 'image') then + call handle_image() + stop 0 else ! Default execute command filename = trim(arg) @@ -900,7 +906,9 @@ contains write(*, '(A)') 'Usage: ./un [options] ' write(*, '(A)') ' ./un session [options]' write(*, '(A)') ' ./un service [options]' + write(*, '(A)') ' ./un image [options]' write(*, '(A)') ' ./un key [--extend]' + write(*, '(A)') ' ./un languages [--json]' write(*, '(A)') '' write(*, '(A)') 'Execute options:' write(*, '(A)') ' -e KEY=VALUE Set environment variable' @@ -928,9 +936,26 @@ contains write(*, '(A)') ' service env export Export vault' write(*, '(A)') ' service env delete Delete vault' write(*, '(A)') '' + write(*, '(A)') 'Image options:' + write(*, '(A)') ' -l, --list List all images' + write(*, '(A)') ' --info ID Get image details' + write(*, '(A)') ' --delete ID Delete an image' + write(*, '(A)') ' --lock ID Lock image' + write(*, '(A)') ' --unlock ID Unlock image' + write(*, '(A)') ' --publish ID Publish (requires --source-type)' + write(*, '(A)') ' --source-type TYPE Source: service or snapshot' + write(*, '(A)') ' --visibility ID MODE Set: private/unlisted/public' + write(*, '(A)') ' --spawn ID Spawn service from image' + write(*, '(A)') ' --clone ID Clone an image' + write(*, '(A)') ' --name NAME Name for spawn/clone' + write(*, '(A)') ' --ports PORTS Ports for spawn' + write(*, '(A)') '' write(*, '(A)') 'Key options:' write(*, '(A)') ' --extend Open browser to extend key' write(*, '(A)') '' + write(*, '(A)') 'Languages options:' + write(*, '(A)') ' --json Output as JSON array' + write(*, '(A)') '' write(*, '(A)') 'Library Usage:' write(*, '(A)') ' use unsandbox_sdk' write(*, '(A)') ' type(unsandbox_client) :: client' @@ -1459,6 +1484,243 @@ contains end if end subroutine handle_service + subroutine handle_image() + character(len=8192) :: full_cmd + character(len=256) :: arg, image_id, operation, source_type, name, ports, visibility_mode + character(len=1024) :: public_key, secret_key + integer :: i, stat + logical :: list_mode + + image_id = '' + operation = '' + source_type = '' + name = '' + ports = '' + visibility_mode = '' + list_mode = .false. + + ! Get credentials + call get_credentials(public_key, secret_key, stat) + if (stat /= 0) then + write(0, '(A)') 'Error: No credentials found' + stop 1 + end if + + ! Parse image arguments + do i = 2, command_argument_count() + call get_command_argument(i, arg) + if (trim(arg) == '-l' .or. trim(arg) == '--list') then + list_mode = .true. + operation = 'list' + else if (trim(arg) == '--info') then + operation = 'info' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--delete') then + operation = 'delete' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--lock') then + operation = 'lock' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--unlock') then + operation = 'unlock' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--publish') then + operation = 'publish' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--source-type') then + if (i < command_argument_count()) then + call get_command_argument(i+1, source_type) + end if + else if (trim(arg) == '--visibility') then + operation = 'visibility' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + if (i+1 < command_argument_count()) then + call get_command_argument(i+2, visibility_mode) + end if + else if (trim(arg) == '--spawn') then + operation = 'spawn' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--clone') then + operation = 'clone' + if (i < command_argument_count()) then + call get_command_argument(i+1, image_id) + end if + else if (trim(arg) == '--name') then + if (i < command_argument_count()) then + call get_command_argument(i+1, name) + end if + else if (trim(arg) == '--ports') then + if (i < command_argument_count()) then + call get_command_argument(i+1, ports) + end if + end if + end do + + if (trim(operation) == 'list') then + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:GET:/images:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X GET https://api.unsandbox.com/images ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" | jq .' + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'info' .and. len_trim(image_id) > 0) then + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:GET:/images/', trim(image_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X GET https://api.unsandbox.com/images/', trim(image_id), ' ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" | jq .' + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'delete' .and. len_trim(image_id) > 0) then + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:DELETE:/images/', trim(image_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X DELETE https://api.unsandbox.com/images/', trim(image_id), ' ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" >/dev/null; ', & + 'echo -e "\x1b[32mImage deleted: ', trim(image_id), '\x1b[0m"' + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'lock' .and. len_trim(image_id) > 0) then + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/lock:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/lock ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" >/dev/null; ', & + 'echo -e "\x1b[32mImage locked: ', trim(image_id), '\x1b[0m"' + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'unlock' .and. len_trim(image_id) > 0) then + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/unlock:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/unlock ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" >/dev/null; ', & + 'echo -e "\x1b[32mImage unlocked: ', trim(image_id), '\x1b[0m"' + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'publish' .and. len_trim(image_id) > 0) then + if (len_trim(source_type) == 0) then + write(0, '(A)') 'Error: --source-type required (service or snapshot)' + stop 1 + end if + if (len_trim(name) > 0) then + write(full_cmd, '(25A)') & + 'BODY=''{"source_type":"', trim(source_type), '","source_id":"', trim(image_id), '","name":"', trim(name), '"}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/publish:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/publish ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mImage published\x1b[0m"' + else + write(full_cmd, '(25A)') & + 'BODY=''{"source_type":"', trim(source_type), '","source_id":"', trim(image_id), '"}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/publish:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/publish ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mImage published\x1b[0m"' + end if + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'visibility' .and. len_trim(image_id) > 0 .and. len_trim(visibility_mode) > 0) then + write(full_cmd, '(25A)') & + 'BODY=''{"visibility":"', trim(visibility_mode), '"}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/visibility:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/visibility ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" >/dev/null; ', & + 'echo -e "\x1b[32mImage visibility set to ', trim(visibility_mode), '\x1b[0m"' + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'spawn' .and. len_trim(image_id) > 0) then + if (len_trim(name) > 0 .and. len_trim(ports) > 0) then + write(full_cmd, '(25A)') & + 'BODY=''{"name":"', trim(name), '","ports":[', trim(ports), ']}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/spawn:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/spawn ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mService spawned from image\x1b[0m"' + else if (len_trim(name) > 0) then + write(full_cmd, '(25A)') & + 'BODY=''{"name":"', trim(name), '"}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/spawn:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/spawn ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mService spawned from image\x1b[0m"' + else + write(full_cmd, '(25A)') & + 'BODY=''{}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/spawn:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/spawn ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mService spawned from image\x1b[0m"' + end if + call execute_command_line(trim(full_cmd), wait=.true.) + else if (trim(operation) == 'clone' .and. len_trim(image_id) > 0) then + if (len_trim(name) > 0) then + write(full_cmd, '(25A)') & + 'BODY=''{"name":"', trim(name), '"}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/clone:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/clone ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mImage cloned\x1b[0m"' + else + write(full_cmd, '(25A)') & + 'BODY=''{}''; ', & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/clone:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/clone ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', & + '-d "$BODY" | jq .; ', & + 'echo -e "\x1b[32mImage cloned\x1b[0m"' + end if + call execute_command_line(trim(full_cmd), wait=.true.) + else + write(0, '(A)') 'Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID' + stop 1 + end if + end subroutine handle_image + subroutine handle_key() character(len=4096) :: full_cmd character(len=256) :: arg @@ -1558,4 +1820,51 @@ contains call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat) end subroutine handle_key + subroutine handle_languages() + character(len=4096) :: full_cmd + character(len=256) :: arg + character(len=1024) :: public_key, secret_key + integer :: i, stat + logical :: json_mode + + json_mode = .false. + + ! Check for --json flag + do i = 2, command_argument_count() + call get_command_argument(i, arg) + if (trim(arg) == '--json') then + json_mode = .true. + end if + end do + + ! Get API keys + call get_credentials(public_key, secret_key, stat) + if (stat /= 0) then + write(0, '(A)') 'Error: No credentials found' + stop 1 + end if + + if (json_mode) then + ! Output as JSON array + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X GET https://api.unsandbox.com/languages ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" ', & + '-H "X-Signature: $SIG" | jq -c ".languages"' + else + ! Output one language per line + write(full_cmd, '(20A)') & + 'TS=$(date +%s); ', & + 'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', & + 'curl -s -X GET https://api.unsandbox.com/languages ', & + '-H "Authorization: Bearer ', trim(public_key), '" ', & + '-H "X-Timestamp: $TS" ', & + '-H "X-Signature: $SIG" | jq -r ".languages[]"' + end if + + call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat) + end subroutine handle_languages + end program unsandbox_cli diff --git a/clients/fsharp/sync/src/un.fs b/clients/fsharp/sync/src/un.fs index fcd2638..86cb11e 100644 --- a/clients/fsharp/sync/src/un.fs +++ b/clients/fsharp/sync/src/un.fs @@ -90,6 +90,7 @@ type Args = { mutable SessionSnapshotName: string option mutable SessionHot: bool mutable ServiceList: bool + mutable LanguagesJson: bool mutable ServiceName: string option mutable ServicePorts: string option mutable ServiceType: string option @@ -123,6 +124,20 @@ type Args = { mutable EnvAction: string option mutable EnvTarget: string option mutable KeyExtend: bool + // Image command options + mutable ImageList: bool + mutable ImageInfo: string option + mutable ImageDelete: string option + mutable ImageLock: string option + mutable ImageUnlock: string option + mutable ImagePublish: string option + mutable ImageSourceType: string option + mutable ImageVisibility: string option + mutable ImageVisibilityMode: string option + mutable ImageSpawn: string option + mutable ImageClone: string option + mutable ImageName: string option + mutable ImagePorts: string option } let getApiKeys (argsKey: string option) = @@ -717,6 +732,93 @@ let cmdKey (args: Args) = printfn "Reason: %s" errorMsg exit 1 +let cmdLanguages (args: Args) = + let (publicKey, secretKey) = getApiKeys args.ApiKey + + let result = apiRequest "/languages" "GET" None publicKey secretKey + + // Extract languages array from the response + match result.TryFind "languages" with + | Some langs -> + // Parse the languages - they come as a string representation + let langStr = langs.ToString() + // Simple parsing for array of strings like: python, javascript, ... + let languages = + if langStr.StartsWith("[") && langStr.EndsWith("]") then + langStr.Substring(1, langStr.Length - 2).Split(',') + |> Array.map (fun s -> s.Trim().Trim('"')) + |> Array.filter (fun s -> not (String.IsNullOrEmpty(s))) + else + [| langStr |] + + if args.LanguagesJson then + // Output as JSON array + let jsonArray = sprintf "[%s]" (languages |> Array.map (sprintf "\"%s\"") |> String.concat ",") + printfn "%s" jsonArray + else + // Output one language per line + for lang in languages do + printfn "%s" lang + | None -> + // Fallback: try to extract from raw JSON using regex + () + +let cmdImage (args: Args) = + let (publicKey, secretKey) = getApiKeys args.ApiKey + + if args.ImageList then + let result = apiRequest "/images" "GET" None publicKey secretKey + printfn "%s" (toJson (box result)) + elif args.ImageInfo.IsSome then + let result = apiRequest (sprintf "/images/%s" args.ImageInfo.Value) "GET" None publicKey secretKey + printfn "%s" (toJson (box result)) + elif args.ImageDelete.IsSome then + let result = apiRequest (sprintf "/images/%s" args.ImageDelete.Value) "DELETE" None publicKey secretKey + printfn "%sImage deleted: %s%s" green args.ImageDelete.Value reset + elif args.ImageLock.IsSome then + let result = apiRequest (sprintf "/images/%s/lock" args.ImageLock.Value) "POST" None publicKey secretKey + printfn "%sImage locked: %s%s" green args.ImageLock.Value reset + elif args.ImageUnlock.IsSome then + let result = apiRequest (sprintf "/images/%s/unlock" args.ImageUnlock.Value) "POST" None publicKey secretKey + printfn "%sImage unlocked: %s%s" green args.ImageUnlock.Value reset + elif args.ImagePublish.IsSome then + if args.ImageSourceType.IsNone then + eprintfn "%sError: --source-type required (service or snapshot)%s" red reset + exit 1 + let mutable payload = [("source_type", box args.ImageSourceType.Value); ("source_id", box args.ImagePublish.Value)] + if args.ImageName.IsSome then + payload <- payload @ [("name", box args.ImageName.Value)] + let result = apiRequest "/images/publish" "POST" (Some payload) publicKey secretKey + printfn "%sImage published%s" green reset + printfn "%s" (toJson (box result)) + elif args.ImageVisibility.IsSome then + if args.ImageVisibilityMode.IsNone then + eprintfn "%sError: --visibility requires MODE (private, unlisted, or public)%s" red reset + exit 1 + let payload = [("visibility", box args.ImageVisibilityMode.Value)] + let result = apiRequest (sprintf "/images/%s/visibility" args.ImageVisibility.Value) "POST" (Some payload) publicKey secretKey + printfn "%sImage visibility set to %s%s" green args.ImageVisibilityMode.Value reset + elif args.ImageSpawn.IsSome then + let mutable payload = [] + if args.ImageName.IsSome then + payload <- payload @ [("name", box args.ImageName.Value)] + if args.ImagePorts.IsSome then + let ports = args.ImagePorts.Value.Split(',') |> Array.map (fun p -> box (int (p.Trim()))) + payload <- payload @ [("ports", box ports)] + let result = apiRequest (sprintf "/images/%s/spawn" args.ImageSpawn.Value) "POST" (Some payload) publicKey secretKey + printfn "%sService spawned from image%s" green reset + printfn "%s" (toJson (box result)) + elif args.ImageClone.IsSome then + let mutable payload = [] + if args.ImageName.IsSome then + payload <- payload @ [("name", box args.ImageName.Value)] + let result = apiRequest (sprintf "/images/%s/clone" args.ImageClone.Value) "POST" (Some payload) publicKey secretKey + printfn "%sImage cloned%s" green reset + printfn "%s" (toJson (box result)) + else + eprintfn "%sError: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID%s" red reset + exit 1 + let cmdSnapshot (args: Args) = let (publicKey, secretKey) = getApiKeys args.ApiKey @@ -906,6 +1008,7 @@ let parseArgs (argv: string[]) = SessionSnapshotName = None SessionHot = false ServiceList = false + LanguagesJson = false ServiceName = None ServicePorts = None ServiceType = None @@ -939,6 +1042,19 @@ let parseArgs (argv: string[]) = EnvAction = None EnvTarget = None KeyExtend = false + ImageList = false + ImageInfo = None + ImageDelete = None + ImageLock = None + ImageUnlock = None + ImagePublish = None + ImageSourceType = None + ImageVisibility = None + ImageVisibilityMode = None + ImageSpawn = None + ImageClone = None + ImageName = None + ImagePorts = None } let mutable i = 0 @@ -947,7 +1063,10 @@ let parseArgs (argv: string[]) = | "session" -> args.Command <- Some "session" | "service" -> args.Command <- Some "service" | "snapshot" -> args.Command <- Some "snapshot" + | "image" -> args.Command <- Some "image" | "key" -> args.Command <- Some "key" + | "languages" -> args.Command <- Some "languages" + | "--json" when args.Command = Some "languages" -> args.LanguagesJson <- true | "env" when args.Command = Some "service" -> // Parse: service env if i + 1 < argv.Length && not (argv.[i + 1].StartsWith("-")) then @@ -968,6 +1087,8 @@ let parseArgs (argv: string[]) = match args.Command with | Some "session" -> args.SessionList <- true | Some "service" -> args.ServiceList <- true + | Some "image" -> args.ImageList <- true + | Some "snapshot" -> args.SnapshotList <- true | _ -> () | "-s" | "--shell" -> i <- i + 1 @@ -1024,11 +1145,13 @@ let parseArgs (argv: string[]) = i <- i + 1 match args.Command with | Some "snapshot" -> args.SnapshotName <- Some argv.[i] + | Some "image" -> args.ImageName <- Some argv.[i] | _ -> args.ServiceName <- Some argv.[i] | "--ports" -> i <- i + 1 match args.Command with | Some "snapshot" -> args.SnapshotPorts <- Some argv.[i] + | Some "image" -> args.ImagePorts <- Some argv.[i] | _ -> args.ServicePorts <- Some argv.[i] | "--bootstrap" -> i <- i + 1; args.ServiceBootstrap <- Some argv.[i] | "--bootstrap-file" -> i <- i + 1; args.ServiceBootstrapFile <- Some argv.[i] @@ -1043,6 +1166,49 @@ let parseArgs (argv: string[]) = | "--dump-bootstrap" -> i <- i + 1; args.ServiceDumpBootstrap <- Some argv.[i] | "--dump-file" -> i <- i + 1; args.ServiceDumpFile <- Some argv.[i] | "--extend" -> args.KeyExtend <- true + | "--info" -> + i <- i + 1 + match args.Command with + | Some "image" -> args.ImageInfo <- Some argv.[i] + | _ -> args.ServiceInfo <- Some argv.[i] + | "--delete" -> + i <- i + 1 + match args.Command with + | Some "image" -> args.ImageDelete <- Some argv.[i] + | Some "snapshot" -> args.SnapshotDelete <- Some argv.[i] + | _ -> () + | "--lock" -> + i <- i + 1 + if args.Command = Some "image" then + args.ImageLock <- Some argv.[i] + | "--unlock" -> + i <- i + 1 + if args.Command = Some "image" then + args.ImageUnlock <- Some argv.[i] + | "--publish" -> + i <- i + 1 + if args.Command = Some "image" then + args.ImagePublish <- Some argv.[i] + | "--source-type" -> + i <- i + 1 + args.ImageSourceType <- Some argv.[i] + | "--visibility" -> + i <- i + 1 + if args.Command = Some "image" then + args.ImageVisibility <- Some argv.[i] + if i + 1 < argv.Length && not (argv.[i + 1].StartsWith("-")) then + i <- i + 1 + args.ImageVisibilityMode <- Some argv.[i] + | "--spawn" -> + i <- i + 1 + if args.Command = Some "image" then + args.ImageSpawn <- Some argv.[i] + | "--clone" -> + i <- i + 1 + match args.Command with + | Some "image" -> args.ImageClone <- Some argv.[i] + | Some "snapshot" -> args.SnapshotClone <- Some argv.[i] + | _ -> () | arg when not (arg.StartsWith("-")) -> args.SourceFile <- Some arg | arg -> if arg.StartsWith("-") && args.Command = Some "session" then @@ -1058,7 +1224,9 @@ let printHelp () = printfn " un session [options]" printfn " un service [options]" printfn " un service env [options]" + printfn " un image [options]" printfn " un key [options]" + printfn " un languages [--json]" printfn "" printfn "Execute options:" printfn " -e KEY=VALUE Set environment variable" @@ -1100,9 +1268,26 @@ let printHelp () = printfn " env export ID Export vault contents" printfn " env delete ID Delete vault" printfn "" + printfn "Image options:" + printfn " -l, --list List all images" + printfn " --info ID Get image details" + printfn " --delete ID Delete an image" + printfn " --lock ID Lock image to prevent deletion" + printfn " --unlock ID Unlock image" + printfn " --publish ID Publish image (requires --source-type)" + printfn " --source-type TYPE Source type: service or snapshot" + printfn " --visibility ID MODE Set visibility: private, unlisted, or public" + printfn " --spawn ID Spawn new service from image" + printfn " --clone ID Clone an image" + printfn " --name NAME Name for spawned service or cloned image" + printfn " --ports PORTS Ports for spawned service" + printfn "" printfn "Key options:" printfn " --extend Open browser to extend key" printfn " -k KEY API key to validate" + printfn "" + printfn "Languages options:" + printfn " --json Output as JSON array" [] let main argv = @@ -1113,7 +1298,9 @@ let main argv = | Some "session" -> cmdSession args; 0 | Some "service" -> cmdService args; 0 | Some "snapshot" -> cmdSnapshot args; 0 + | Some "image" -> cmdImage args; 0 | Some "key" -> cmdKey args; 0 + | Some "languages" -> cmdLanguages args; 0 | _ -> match args.SourceFile with | Some _ -> cmdExecute args; 0 diff --git a/clients/go/sync/src/un.go b/clients/go/sync/src/un.go index dc1c699..404a247 100644 --- a/clients/go/sync/src/un.go +++ b/clients/go/sync/src/un.go @@ -1379,6 +1379,7 @@ Usage: un service [options] Manage services un snapshot [options] Manage snapshots un key Check API key + un languages [--json] List available languages Global Options: -s, --shell LANG Language for inline code @@ -1401,6 +1402,8 @@ Examples: un -n semitrusted crawler.py With network access un session --tmux Persistent interactive session un service --list List all services + un languages List available languages + un languages --json List languages as JSON `) } @@ -1519,6 +1522,39 @@ Examples: `) } +// printImageUsage prints image subcommand help +func printImageUsage() { + fmt.Fprintf(os.Stderr, `un image - Image management + +Usage: + un image --list List all images + un image --info ID Get details + un image --delete ID Delete image + un image --publish ID Publish image from service/snapshot + +Options: + -l, --list List all images + --info ID Get image details + --delete ID Delete image + --lock ID Prevent deletion + --unlock ID Allow deletion + --publish ID Publish image (requires --source-type) + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility (private, unlisted, public) + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + +Examples: + un image --list + un image --info abc123 + un image --publish svc123 --source-type service --name myimage + un image --spawn img123 --name myservice --ports 80,443 + un image --visibility img123 public +`) +} + // cliError prints error to stderr and returns exit code func cliError(msg string, code int) int { fmt.Fprintf(os.Stderr, "Error: %s\n", msg) @@ -2506,6 +2542,243 @@ func parseSnapshotFlags(args []string, fs *snapshotFlags) { } } +type imageFlags struct { + list bool + info string + deleteID string + lock string + unlock string + publish string + sourceType string + visibility string + visMode string + spawn string + clone string + name string + ports string + help bool +} + +func parseImageFlags(args []string, fs *imageFlags) { + for i := 0; i < len(args); i++ { + arg := args[i] + switch arg { + case "-l", "--list": + fs.list = true + case "--info": + if i+1 < len(args) { + fs.info = args[i+1] + i++ + } + case "--delete": + if i+1 < len(args) { + fs.deleteID = args[i+1] + i++ + } + case "--lock": + if i+1 < len(args) { + fs.lock = args[i+1] + i++ + } + case "--unlock": + if i+1 < len(args) { + fs.unlock = args[i+1] + i++ + } + case "--publish": + if i+1 < len(args) { + fs.publish = args[i+1] + i++ + } + case "--source-type": + if i+1 < len(args) { + fs.sourceType = args[i+1] + i++ + } + case "--visibility": + if i+1 < len(args) { + fs.visibility = args[i+1] + i++ + } + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + fs.visMode = args[i+1] + i++ + } + case "--spawn": + if i+1 < len(args) { + fs.spawn = args[i+1] + i++ + } + case "--clone": + if i+1 < len(args) { + fs.clone = args[i+1] + i++ + } + case "--name": + if i+1 < len(args) { + fs.name = args[i+1] + i++ + } + case "--ports": + if i+1 < len(args) { + fs.ports = args[i+1] + i++ + } + case "-h", "--help": + fs.help = true + } + } +} + +func runImage(creds *Credentials, args []string, opts *CLIOptions) int { + fs := &imageFlags{} + parseImageFlags(args, fs) + + if fs.help { + printImageUsage() + return ExitSuccess + } + + // List images + if fs.list { + images, err := ListImages(creds, "") + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + formatList(images, []string{"image_id", "name", "visibility", "source_type", "created_at"}) + return ExitSuccess + } + + // Get image info + if fs.info != "" { + image, err := GetImage(creds, fs.info) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + jsonOut, _ := json.MarshalIndent(image, "", " ") + fmt.Println(string(jsonOut)) + return ExitSuccess + } + + // Delete image + if fs.deleteID != "" { + _, err := DeleteImage(creds, fs.deleteID) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + fmt.Printf("Image %s deleted\n", fs.deleteID) + return ExitSuccess + } + + // Lock image + if fs.lock != "" { + _, err := LockImage(creds, fs.lock) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + fmt.Printf("Image %s locked\n", fs.lock) + return ExitSuccess + } + + // Unlock image + if fs.unlock != "" { + _, err := UnlockImage(creds, fs.unlock) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + fmt.Printf("Image %s unlocked\n", fs.unlock) + return ExitSuccess + } + + // Publish image + if fs.publish != "" { + if fs.sourceType == "" { + return cliError("--source-type required for --publish", ExitInvalidArgs) + } + pubOpts := &ImagePublishOptions{} + if fs.name != "" { + pubOpts.Name = fs.name + } + result, err := ImagePublish(creds, fs.sourceType, fs.publish, pubOpts) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + imageID := "" + if id, ok := result["image_id"].(string); ok { + imageID = id + } else if id, ok := result["id"].(string); ok { + imageID = id + } + fmt.Printf("Image published: %s\n", imageID) + return ExitSuccess + } + + // Set visibility + if fs.visibility != "" && fs.visMode != "" { + if fs.visMode != "private" && fs.visMode != "unlisted" && fs.visMode != "public" { + return cliError("visibility must be private, unlisted, or public", ExitInvalidArgs) + } + _, err := SetImageVisibility(creds, fs.visibility, fs.visMode) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + fmt.Printf("Image %s visibility set to %s\n", fs.visibility, fs.visMode) + return ExitSuccess + } + + // Spawn from image + if fs.spawn != "" { + if fs.name == "" { + return cliError("--name required for --spawn", ExitInvalidArgs) + } + spawnOpts := &SpawnFromImageOptions{ + Name: fs.name, + } + if fs.ports != "" { + ports, err := parsePorts(fs.ports) + if err != nil { + return cliError(err.Error(), ExitInvalidArgs) + } + spawnOpts.Ports = ports + } + result, err := SpawnFromImage(creds, fs.spawn, spawnOpts) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + serviceID := "" + if id, ok := result["service_id"].(string); ok { + serviceID = id + } else if id, ok := result["id"].(string); ok { + serviceID = id + } + fmt.Printf("Service spawned: %s\n", serviceID) + return ExitSuccess + } + + // Clone image + if fs.clone != "" { + cloneOpts := &CloneImageOptions{} + if fs.name != "" { + cloneOpts.Name = fs.name + } + result, err := CloneImage(creds, fs.clone, cloneOpts) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + imageID := "" + if id, ok := result["image_id"].(string); ok { + imageID = id + } else if id, ok := result["id"].(string); ok { + imageID = id + } + fmt.Printf("Image cloned: %s\n", imageID) + return ExitSuccess + } + + printImageUsage() + return ExitInvalidArgs +} + // runKey handles the key command func runKey(creds *Credentials) int { result, err := ValidateKeys(creds) @@ -2521,6 +2794,34 @@ func runKey(creds *Credentials) int { return ExitSuccess } +// runLanguages handles the languages command +func runLanguages(creds *Credentials, args []string) int { + // Check for --json flag + jsonOutput := false + for _, arg := range args { + if arg == "--json" { + jsonOutput = true + } + } + + languages, err := GetLanguages(creds) + if err != nil { + return cliError(err.Error(), ExitAPIError) + } + + if jsonOutput { + // Output as JSON array + jsonOut, _ := json.Marshal(languages) + fmt.Println(string(jsonOut)) + } else { + // Output one language per line (pipe-friendly) + for _, lang := range languages { + fmt.Println(lang) + } + } + return ExitSuccess +} + // parseGlobalFlags parses global CLI options func parseGlobalFlags(args []string) (*CLIOptions, []string) { opts := &CLIOptions{} @@ -2609,7 +2910,7 @@ func CliMain() { cmdArgs := remaining if len(remaining) > 0 { switch remaining[0] { - case "session", "service", "snapshot", "key": + case "session", "service", "snapshot", "key", "languages": command = remaining[0] cmdArgs = remaining[1:] default: @@ -2641,8 +2942,12 @@ func CliMain() { exitCode = runService(creds, cmdArgs, opts) case "snapshot": exitCode = runSnapshot(creds, cmdArgs, opts) + case "image": + exitCode = runImage(creds, cmdArgs, opts) case "key": exitCode = runKey(creds) + case "languages": + exitCode = runLanguages(creds, cmdArgs) default: printUsage() exitCode = ExitInvalidArgs diff --git a/clients/groovy/sync/src/un.groovy b/clients/groovy/sync/src/un.groovy index 6011ed1..a16e49c 100644 --- a/clients/groovy/sync/src/un.groovy +++ b/clients/groovy/sync/src/un.groovy @@ -1020,10 +1020,24 @@ class Args { String snapshotShell = null String snapshotPorts = null Boolean keyExtend = false + Boolean imageList = false + String imageInfo = null + String imageDelete = null + String imageLock = null + String imageUnlock = null + String imagePublish = null + String imageSourceType = null + String imageVisibility = null + String imageVisibilityMode = null + String imageSpawn = null + String imageClone = null + String imageName = null + String imagePorts = null List svcEnvs = [] String svcEnvFile = null String envAction = null String envTarget = null + Boolean jsonOutput = false } def readEnvFile(filename) { @@ -1293,6 +1307,101 @@ def cmdSnapshot(args) { System.exit(1) } +def cmdImage(args) { + def (publicKey, secretKey) = getApiKeys(args.apiKey) + + if (args.imageList) { + def output = apiRequest('/images', 'GET', null, publicKey, secretKey) + println(JsonOutput.prettyPrint(JsonOutput.toJson(output))) + return + } + + if (args.imageInfo) { + def output = apiRequest("/images/${args.imageInfo}", 'GET', null, publicKey, secretKey) + println(JsonOutput.prettyPrint(JsonOutput.toJson(output))) + return + } + + if (args.imageDelete) { + apiRequest("/images/${args.imageDelete}", 'DELETE', null, publicKey, secretKey) + println("${GREEN}Image deleted: ${args.imageDelete}${RESET}") + return + } + + if (args.imageLock) { + apiRequest("/images/${args.imageLock}/lock", 'POST', null, publicKey, secretKey) + println("${GREEN}Image locked: ${args.imageLock}${RESET}") + return + } + + if (args.imageUnlock) { + apiRequest("/images/${args.imageUnlock}/unlock", 'POST', null, publicKey, secretKey) + println("${GREEN}Image unlocked: ${args.imageUnlock}${RESET}") + return + } + + if (args.imagePublish) { + if (!args.imageSourceType) { + System.err.println("${RED}Error: --source-type required (service or snapshot)${RESET}") + System.exit(1) + } + def payload = [source_type: args.imageSourceType, source_id: args.imagePublish] + if (args.imageName) payload.name = args.imageName + def output = apiRequest("/images/publish", 'POST', payload, publicKey, secretKey) + println("${GREEN}Image published${RESET}") + println(JsonOutput.prettyPrint(JsonOutput.toJson(output))) + return + } + + if (args.imageVisibility) { + if (!args.imageVisibilityMode) { + System.err.println("${RED}Error: --visibility requires MODE (private, unlisted, or public)${RESET}") + System.exit(1) + } + def payload = [visibility: args.imageVisibilityMode] + apiRequest("/images/${args.imageVisibility}/visibility", 'POST', payload, publicKey, secretKey) + println("${GREEN}Image visibility set to ${args.imageVisibilityMode}: ${args.imageVisibility}${RESET}") + return + } + + if (args.imageSpawn) { + def payload = [:] + if (args.imageName) payload.name = args.imageName + if (args.imagePorts) payload.ports = args.imagePorts.split(',').collect { it.trim().toInteger() } + def output = apiRequest("/images/${args.imageSpawn}/spawn", 'POST', payload, publicKey, secretKey) + println("${GREEN}Service spawned from image${RESET}") + println(JsonOutput.prettyPrint(JsonOutput.toJson(output))) + return + } + + if (args.imageClone) { + def payload = [:] + if (args.imageName) payload.name = args.imageName + def output = apiRequest("/images/${args.imageClone}/clone", 'POST', payload, publicKey, secretKey) + println("${GREEN}Image cloned${RESET}") + println(JsonOutput.prettyPrint(JsonOutput.toJson(output))) + return + } + + System.err.println("${RED}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${RESET}") + System.exit(1) +} + +def cmdLanguages(args) { + def (publicKey, secretKey) = getApiKeys(args.apiKey) + + def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true]) + def langList = result.languages ?: [] + + if (args.jsonOutput) { + println(JsonOutput.toJson(langList)) + } else { + langList.each { lang -> + println(lang) + } + } +} + def cmdKey(args) { def (publicKey, secretKey) = getApiKeys(args.apiKey) @@ -1540,6 +1649,9 @@ def parseArgs(argv) { def i = 0 while (i < argv.size()) { switch (argv[i]) { + case 'languages': + args.command = 'languages' + break case 'session': args.command = 'session' break @@ -1555,6 +1667,9 @@ def parseArgs(argv) { case 'snapshot': args.command = 'snapshot' break + case 'image': + args.command = 'image' + break case 'key': args.command = 'key' break @@ -1605,6 +1720,7 @@ def parseArgs(argv) { if (args.command == 'session') args.sessionList = true else if (args.command == 'service') args.serviceList = true else if (args.command == 'snapshot') args.snapshotList = true + else if (args.command == 'image') args.imageList = true break case '--shell': if (args.command == 'snapshot') args.snapshotShell = argv[++i] @@ -1635,13 +1751,39 @@ def parseArgs(argv) { break case '--info': if (args.command == 'snapshot') args.snapshotInfo = argv[++i] + else if (args.command == 'image') args.imageInfo = argv[++i] else args.serviceInfo = argv[++i] break case '--delete': if (args.command == 'snapshot') args.snapshotDelete = argv[++i] + else if (args.command == 'image') args.imageDelete = argv[++i] break case '--clone': - args.snapshotClone = argv[++i] + if (args.command == 'image') args.imageClone = argv[++i] + else args.snapshotClone = argv[++i] + break + case '--lock': + if (args.command == 'image') args.imageLock = argv[++i] + break + case '--unlock': + if (args.command == 'image') args.imageUnlock = argv[++i] + break + case '--publish': + if (args.command == 'image') args.imagePublish = argv[++i] + break + case '--source-type': + args.imageSourceType = argv[++i] + break + case '--visibility': + if (args.command == 'image') { + args.imageVisibility = argv[++i] + if (i + 1 < argv.size() && !argv[i + 1].startsWith('-')) { + args.imageVisibilityMode = argv[++i] + } + } + break + case '--spawn': + if (args.command == 'image') args.imageSpawn = argv[++i] break case '--type': if (args.command == 'snapshot') args.snapshotType = argv[++i] @@ -1649,10 +1791,12 @@ def parseArgs(argv) { break case '--name': if (args.command == 'snapshot') args.snapshotName = argv[++i] + else if (args.command == 'image') args.imageName = argv[++i] else args.serviceName = argv[++i] break case '--ports': if (args.command == 'snapshot') args.snapshotPorts = argv[++i] + else if (args.command == 'image') args.imagePorts = argv[++i] else args.servicePorts = argv[++i] break case '--bootstrap': @@ -1694,6 +1838,9 @@ def parseArgs(argv) { case '--extend': args.keyExtend = true break + case '--json': + args.jsonOutput = true + break default: if (argv[i].startsWith('-')) { System.err.println("${RED}Unknown option: ${argv[i]}${RESET}") @@ -1716,6 +1863,8 @@ Usage: groovy un.groovy [options] groovy un.groovy session [options] groovy un.groovy service [options] groovy un.groovy service env [options] + groovy un.groovy image [options] + groovy un.groovy languages [--json] groovy un.groovy key [options] Execute options: @@ -1765,6 +1914,23 @@ Vault commands: Key options: --extend Open browser to extend key +Image options: + --list List images + --info ID Get image details + --delete ID Delete an image + --lock ID Lock image to prevent deletion + --unlock ID Unlock image + --publish ID Publish image from service/snapshot (requires --source-type) + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility: private, unlisted, or public + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + +Languages options: + --json Output as JSON array + Library Usage: import un def result = un.execute("python", 'print("Hello")') @@ -1779,7 +1945,9 @@ Library Usage: try { def args = parseArgs(this.args as List) - if (args.command == 'session') { + if (args.command == 'languages') { + cmdLanguages(args) + } else if (args.command == 'session') { cmdSession(args) } else if (args.command == 'service') { if (args.envAction && args.envTarget) { @@ -1789,6 +1957,8 @@ try { } } else if (args.command == 'snapshot') { cmdSnapshot(args) + } else if (args.command == 'image') { + cmdImage(args) } else if (args.command == 'key') { cmdKey(args) } else if (args.sourceFile || args.inlineLang) { diff --git a/clients/haskell/sync/src/un.hs b/clients/haskell/sync/src/un.hs index 76a63b7..a5a9960 100644 --- a/clients/haskell/sync/src/un.hs +++ b/clients/haskell/sync/src/un.hs @@ -116,7 +116,11 @@ escapeJSON = concatMap escape escape c = [c] -- Parse command line arguments -data Command = Execute ExecuteOpts | Session SessionOpts | Service ServiceOpts | Key KeyOpts | Snapshot SnapshotOpts | Help +data Command = Execute ExecuteOpts | Session SessionOpts | Service ServiceOpts | Key KeyOpts | Snapshot SnapshotOpts | Image ImageOpts | Languages LanguagesOpts | Help + +data LanguagesOpts = LanguagesOpts + { langJson :: Bool + } data ExecuteOpts = ExecuteOpts { exFile :: String @@ -175,6 +179,18 @@ data SnapshotOpts = SnapshotOpts data SnapshotAction = SnapshotList | SnapshotInfo String | SnapshotDelete String | SnapshotClone String +data ImageOpts = ImageOpts + { imgAction :: ImageAction + , imgName :: Maybe String + , imgPorts :: Maybe String + , imgSourceType :: Maybe String + , imgVisibilityMode :: Maybe String + } + +data ImageAction = ImageList | ImageInfo String | ImageDelete String + | ImageLock String | ImageUnlock String | ImagePublish String + | ImageVisibility String | ImageSpawn String | ImageClone String + data KeyOpts = KeyOpts { keyExtend :: Bool } @@ -185,8 +201,18 @@ parseArgs ("session":rest) = Session <$> parseSession rest parseArgs ("service":rest) = Service <$> parseService rest parseArgs ("key":rest) = Key <$> parseKey rest parseArgs ("snapshot":rest) = Snapshot <$> parseSnapshot rest +parseArgs ("image":rest) = Image <$> parseImage rest +parseArgs ("languages":rest) = Languages <$> parseLanguages rest parseArgs args = parseExecute args +parseLanguages :: [String] -> IO LanguagesOpts +parseLanguages args = return $ parseLanguagesArgs args defaultLanguagesOpts + where + defaultLanguagesOpts = LanguagesOpts False + parseLanguagesArgs [] opts = opts + parseLanguagesArgs ("--json":rest) opts = parseLanguagesArgs rest opts { langJson = True } + parseLanguagesArgs (_:rest) opts = parseLanguagesArgs rest opts + parseKey :: [String] -> IO KeyOpts parseKey args = return $ parseKeyArgs args defaultKeyOpts where @@ -210,6 +236,26 @@ parseSnapshot args = return $ parseSnapshotArgs args defaultSnapshotOpts parseSnapshotArgs ("--ports":p:rest) opts = parseSnapshotArgs rest opts { snapClonePorts = Just p } parseSnapshotArgs (_:rest) opts = parseSnapshotArgs rest opts +parseImage :: [String] -> IO ImageOpts +parseImage args = return $ parseImageArgs args defaultImageOpts + where + defaultImageOpts = ImageOpts ImageList Nothing Nothing Nothing Nothing + parseImageArgs [] opts = opts + parseImageArgs ("--list":rest) opts = parseImageArgs rest opts { imgAction = ImageList } + parseImageArgs ("-l":rest) opts = parseImageArgs rest opts { imgAction = ImageList } + parseImageArgs ("--info":id:rest) opts = parseImageArgs rest opts { imgAction = ImageInfo id } + parseImageArgs ("--delete":id:rest) opts = parseImageArgs rest opts { imgAction = ImageDelete id } + parseImageArgs ("--lock":id:rest) opts = parseImageArgs rest opts { imgAction = ImageLock id } + parseImageArgs ("--unlock":id:rest) opts = parseImageArgs rest opts { imgAction = ImageUnlock id } + parseImageArgs ("--publish":id:rest) opts = parseImageArgs rest opts { imgAction = ImagePublish id } + parseImageArgs ("--source-type":t:rest) opts = parseImageArgs rest opts { imgSourceType = Just t } + parseImageArgs ("--visibility":id:mode:rest) opts = parseImageArgs rest opts { imgAction = ImageVisibility id, imgVisibilityMode = Just mode } + parseImageArgs ("--spawn":id:rest) opts = parseImageArgs rest opts { imgAction = ImageSpawn id } + parseImageArgs ("--clone":id:rest) opts = parseImageArgs rest opts { imgAction = ImageClone id } + parseImageArgs ("--name":n:rest) opts = parseImageArgs rest opts { imgName = Just n } + parseImageArgs ("--ports":p:rest) opts = parseImageArgs rest opts { imgPorts = Just p } + parseImageArgs (_:rest) opts = parseImageArgs rest opts + parseSession :: [String] -> IO SessionOpts parseSession args = return $ parseSessionArgs args defaultSessionOpts where @@ -300,6 +346,8 @@ main = do Service opts -> serviceCommand opts Key opts -> keyCommand opts Snapshot opts -> snapshotCommand opts + Image opts -> imageCommand opts + Languages opts -> languagesCommand opts Help -> printHelp printHelp :: IO () @@ -310,6 +358,8 @@ printHelp = do putStrLn " un.hs service [options] Manage services" putStrLn " un.hs service env Manage service vault" putStrLn " un.hs snapshot [options] Manage snapshots" + putStrLn " un.hs image [options] Manage images" + putStrLn " un.hs languages [--json] List available languages" putStrLn " un.hs key [options] Validate/extend API key" putStrLn "" putStrLn "Execute options:" @@ -359,6 +409,23 @@ printHelp = do putStrLn "" putStrLn "Key options:" putStrLn " --extend Open browser to extend/renew key" + putStrLn "" + putStrLn "Image options:" + putStrLn " -l, --list List all images" + putStrLn " --info ID Get image details" + putStrLn " --delete ID Delete an image" + putStrLn " --lock ID Lock image to prevent deletion" + putStrLn " --unlock ID Unlock image" + putStrLn " --publish ID Publish image from service/snapshot (requires --source-type)" + putStrLn " --source-type TYPE Source type: service or snapshot" + putStrLn " --visibility ID MODE Set visibility: private, unlisted, or public" + putStrLn " --spawn ID Spawn new service from image" + putStrLn " --clone ID Clone an image" + putStrLn " --name NAME Name for spawned service or cloned image" + putStrLn " --ports PORTS Ports for spawned service" + putStrLn "" + putStrLn "Languages options:" + putStrLn " --json Output as JSON array" exitFailure -- Execute command @@ -864,6 +931,101 @@ snapshotCommand opts = do putStrLn $ green ++ "Snapshot cloned" ++ reset putStrLn stdout +-- Image command +imageCommand :: ImageOpts -> IO () +imageCommand opts = do + apiKey <- getApiKey + case imgAction opts of + ImageList -> do + (_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/images" + putStrLn stdout + ImageInfo iid -> do + (_, stdout, _) <- curlGet apiKey ("https://api.unsandbox.com/images/" ++ iid) + putStrLn stdout + ImageDelete iid -> do + (_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/images/" ++ iid) + putStrLn $ green ++ "Image deleted: " ++ iid ++ reset + ImageLock iid -> do + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/images/" ++ iid ++ "/lock") "{}" + putStrLn $ green ++ "Image locked: " ++ iid ++ reset + ImageUnlock iid -> do + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/images/" ++ iid ++ "/unlock") "{}" + putStrLn $ green ++ "Image unlocked: " ++ iid ++ reset + ImagePublish sourceId -> do + case imgSourceType opts of + Nothing -> do + hPutStrLn stderr $ red ++ "Error: --source-type required (service or snapshot)" ++ reset + exitFailure + Just sourceType -> do + let nameJSON = maybe "" (\n -> ",\"name\":\"" ++ escapeJSON n ++ "\"") (imgName opts) + let json = "{\"source_type\":\"" ++ sourceType ++ "\",\"source_id\":\"" ++ sourceId ++ "\"" ++ nameJSON ++ "}" + (_, stdout, _) <- curlPost apiKey "https://api.unsandbox.com/images/publish" json + putStrLn $ green ++ "Image published" ++ reset + putStrLn stdout + ImageVisibility iid -> do + case imgVisibilityMode opts of + Nothing -> do + hPutStrLn stderr $ red ++ "Error: --visibility requires MODE (private, unlisted, or public)" ++ reset + exitFailure + Just mode -> do + let json = "{\"visibility\":\"" ++ mode ++ "\"}" + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/images/" ++ iid ++ "/visibility") json + putStrLn $ green ++ "Image visibility set to " ++ mode ++ ": " ++ iid ++ reset + ImageSpawn iid -> do + let nameJSON = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\"") (imgName opts) + let portsJSON = maybe "" (\p -> ",\"ports\":[" ++ p ++ "]") (imgPorts opts) + let json = if null nameJSON then "{" ++ (if null portsJSON then "" else drop 1 portsJSON) ++ "}" + else "{" ++ nameJSON ++ portsJSON ++ "}" + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/images/" ++ iid ++ "/spawn") json + putStrLn $ green ++ "Service spawned from image" ++ reset + putStrLn stdout + ImageClone iid -> do + let nameJSON = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\"") (imgName opts) + let json = "{" ++ nameJSON ++ "}" + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/images/" ++ iid ++ "/clone") json + putStrLn $ green ++ "Image cloned" ++ reset + putStrLn stdout + +-- Languages command +languagesCommand :: LanguagesOpts -> IO () +languagesCommand opts = do + apiKey <- getApiKey + (_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/languages" + let jsonOutput = langJson opts + if jsonOutput + then do + -- Extract languages array and print as JSON + case extractJsonArray stdout "languages" of + Just langs -> putStrLn $ "[" ++ intercalate "," (map (\l -> "\"" ++ l ++ "\"") langs) ++ "]" + Nothing -> putStrLn stdout + else do + -- Print each language on its own line + case extractJsonArray stdout "languages" of + Just langs -> mapM_ putStrLn langs + Nothing -> putStrLn stdout + +-- Extract JSON array of strings from response +extractJsonArray :: String -> String -> Maybe [String] +extractJsonArray json field = + let needle = "\"" ++ field ++ "\":[" + rest = dropWhile (not . isPrefixOf needle) (tails json) + in case rest of + (x:_) -> Just $ parseArrayItems $ drop (length needle) x + _ -> Nothing + where + tails [] = [[]] + tails s@(_:xs) = s : tails xs + + parseArrayItems :: String -> [String] + parseArrayItems s = go (dropWhile (`elem` " \t\n") s) [] + where + go (']':_) acc = reverse acc + go ('"':rest) acc = + let (item, remaining) = span (/= '"') rest + in go (dropWhile (`elem` ",] \t\n") (drop 1 remaining)) (item : acc) + go (_:rest) acc = go rest acc + go [] acc = reverse acc + -- Key command keyCommand :: KeyOpts -> IO () keyCommand opts = do diff --git a/clients/java/sync/src/Un.java b/clients/java/sync/src/Un.java index 122347b..46820bb 100644 --- a/clients/java/sync/src/Un.java +++ b/clients/java/sync/src/Un.java @@ -1895,6 +1895,36 @@ public class Un { 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). * @@ -2335,9 +2365,15 @@ public class Un { 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); @@ -2354,7 +2390,9 @@ public class Un { 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"); @@ -2408,6 +2446,23 @@ public class Un { 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( @@ -2993,11 +3048,210 @@ public class Un { } } + 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) { diff --git a/clients/javascript/sync/src/un.js b/clients/javascript/sync/src/un.js index 003a308..7a1c4f8 100644 --- a/clients/javascript/sync/src/un.js +++ b/clients/javascript/sync/src/un.js @@ -1410,6 +1410,7 @@ USAGE: node un.js service [options] Service management node un.js snapshot [options] Snapshot management node un.js key Check API key validity + node un.js languages [--json] List available languages GLOBAL OPTIONS: -s, --shell Language for inline code execution @@ -1534,6 +1535,14 @@ function parseArgs(args) { // Snapshot options delete: null, clone: null, + // Image options + publish: null, + sourceType: null, + visibility: null, + visibilityMode: null, + spawn: null, + // Languages options + json: false, }; let i = 0; @@ -1563,6 +1572,16 @@ function parseArgs(args) { i++; continue; } + if (arg === 'languages' && result.command === null) { + result.command = 'languages'; + i++; + continue; + } + if (arg === 'image' && result.command === null) { + result.command = 'image'; + i++; + continue; + } // Service env subcommand if (arg === 'env' && result.command === 'service') { result.subcommand = 'env'; @@ -1615,6 +1634,9 @@ function parseArgs(args) { } else if (arg === '-y' || arg === '--yes') { result.yes = true; i++; + } else if (arg === '--json') { + result.json = true; + i++; } else if (arg === '-l' || arg === '--list') { result.list = true; i++; @@ -1712,6 +1734,22 @@ function parseArgs(args) { } else if (arg === '--clone') { result.clone = args[++i]; i++; + } else if (arg === '--publish') { + result.publish = args[++i]; + i++; + } else if (arg === '--source-type') { + result.sourceType = args[++i]; + i++; + } else if (arg === '--visibility') { + result.visibility = args[++i]; + // Next arg might be the mode + if (i + 1 < args.length && !args[i + 1].startsWith('-')) { + result.visibilityMode = args[++i]; + } + i++; + } else if (arg === '--spawn') { + result.spawn = args[++i]; + i++; } else if (arg.startsWith('-')) { console.error(`Error: Unknown option: ${arg}`); process.exit(2); @@ -2136,6 +2174,110 @@ async function handleSnapshot(opts) { process.exit(2); } +/** + * Handle image command. + */ +async function handleImage(opts) { + const pk = opts.publicKey; + const sk = opts.secretKey; + + // List images + if (opts.list) { + const images = await listImages(null, pk, sk); + formatTable(images, [ + { key: 'image_id', label: 'ID' }, + { key: 'name', label: 'NAME' }, + { key: 'visibility', label: 'VISIBILITY' }, + { key: 'source_type', label: 'SOURCE' }, + { key: 'created_at', label: 'CREATED', getter: (s) => formatTimestamp(s.created_at) }, + ]); + return; + } + + // Get image info + if (opts.info) { + const image = await getImage(opts.info, pk, sk); + console.log(JSON.stringify(image, null, 2)); + return; + } + + // Delete image + if (opts.delete) { + await deleteImage(opts.delete, pk, sk); + console.log(`Image ${opts.delete} deleted.`); + return; + } + + // Lock image + if (opts.lock) { + await lockImage(opts.lock, pk, sk); + console.log(`Image ${opts.lock} locked.`); + return; + } + + // Unlock image + if (opts.unlock) { + await unlockImage(opts.unlock, pk, sk); + console.log(`Image ${opts.unlock} unlocked.`); + return; + } + + // Publish image + if (opts.publish) { + if (!opts.sourceType) { + console.error('Error: --source-type required for --publish'); + process.exit(2); + } + const pubOpts = { publicKey: pk, secretKey: sk }; + if (opts.name) pubOpts.name = opts.name; + const result = await imagePublish(opts.sourceType, opts.publish, pubOpts); + const imageId = result.image_id || result.id; + console.log(`Image published: ${imageId}`); + return; + } + + // Set visibility + if (opts.visibility && opts.visibilityMode) { + if (!['private', 'unlisted', 'public'].includes(opts.visibilityMode)) { + console.error('Error: visibility must be private, unlisted, or public'); + process.exit(2); + } + await setImageVisibility(opts.visibility, opts.visibilityMode, pk, sk); + console.log(`Image ${opts.visibility} visibility set to ${opts.visibilityMode}.`); + return; + } + + // Spawn from image + if (opts.spawn) { + if (!opts.name) { + console.error('Error: --name required for --spawn'); + process.exit(2); + } + const spawnOpts = { publicKey: pk, secretKey: sk, name: opts.name }; + if (opts.ports) { + spawnOpts.ports = opts.ports.split(',').map((p) => parseInt(p.trim(), 10)); + } + const result = await spawnFromImage(opts.spawn, spawnOpts); + const serviceId = result.service_id || result.id; + console.log(`Service spawned: ${serviceId}`); + return; + } + + // Clone image + if (opts.clone) { + const cloneOpts = { publicKey: pk, secretKey: sk }; + if (opts.name) cloneOpts.name = opts.name; + const result = await cloneImage(opts.clone, cloneOpts); + const imageId = result.image_id || result.id; + console.log(`Image cloned: ${imageId}`); + return; + } + + // No action specified + console.error('Error: No image action specified. Use --list, --info, --delete, --publish, --spawn, --clone, etc.'); + process.exit(2); +} + /** * Handle key command. */ @@ -2152,6 +2294,23 @@ async function handleKey(opts) { } } +/** + * Handle languages command. + */ +async function handleLanguages(opts) { + const languages = await getLanguages(opts.publicKey, opts.secretKey); + + if (opts.json) { + // Output as JSON array + console.log(JSON.stringify(languages)); + } else { + // Output one language per line (pipe-friendly) + for (const lang of languages) { + console.log(lang); + } + } +} + /** * Handle execute command (default). */ @@ -2269,9 +2428,15 @@ async function cliMain() { case 'snapshot': await handleSnapshot(opts); break; + case 'image': + await handleImage(opts); + break; case 'key': await handleKey(opts); break; + case 'languages': + await handleLanguages(opts); + break; default: await handleExecute(opts); } diff --git a/clients/julia/sync/src/un.jl b/clients/julia/sync/src/un.jl index efee0d3..9a72e46 100755 --- a/clients/julia/sync/src/un.jl +++ b/clients/julia/sync/src/un.jl @@ -762,6 +762,21 @@ function validate_key(api_key::String) end end +function cmd_languages(args) + (public_key, secret_key) = get_api_keys(args["api-key"]) + + result = api_request("/languages", public_key, secret_key) + langs = get(result, "languages", []) + + if args["json"] + println(JSON.json(langs)) + else + for lang in langs + println(lang) + end + end +end + function cmd_key(args) (public_key, secret_key) = get_api_keys(args["api-key"]) # For portal validation, we still use public_key as bearer token @@ -850,9 +865,15 @@ function main() "service" help = "Manage persistent services" action = :command + "languages" + help = "List available programming languages" + action = :command "key" help = "Check API key validity and expiration" action = :command + "image" + help = "Manage images" + action = :command end @add_arg_table! s["session"] begin @@ -956,6 +977,46 @@ function main() help = "API key" end + @add_arg_table! s["languages"] begin + "--json" + help = "Output as JSON array" + action = :store_true + "--api-key", "-k" + help = "API key" + end + + @add_arg_table! s["image"] begin + "--list", "-l" + help = "List all images" + action = :store_true + "--info" + help = "Get image details" + "--delete" + help = "Delete an image" + "--lock" + help = "Lock image to prevent deletion" + "--unlock" + help = "Unlock image" + "--publish" + help = "Publish image from service/snapshot (requires --source-type)" + "--source-type" + help = "Source type: service or snapshot" + "--visibility" + help = "Image ID to set visibility for" + "--visibility-mode" + help = "Visibility mode: private, unlisted, or public" + "--spawn" + help = "Spawn new service from image" + "--clone" + help = "Clone an image" + "--name" + help = "Name for spawned service or cloned image" + "--ports" + help = "Comma-separated ports for spawned service" + "--api-key", "-k" + help = "API key" + end + args = parse_args(ARGS, s) if args["%COMMAND%"] == "session" @@ -973,14 +1034,109 @@ function main() service_args["api-key"] = get(env_args, "api-key", nothing) end cmd_service(service_args) + elseif args["%COMMAND%"] == "languages" + cmd_languages(args["languages"]) elseif args["%COMMAND%"] == "key" cmd_key(args["key"]) + elseif args["%COMMAND%"] == "image" + cmd_image(args["image"]) elseif args["source_file"] !== nothing cmd_execute(args) else - println(stderr, "$(RED)Error: Provide source_file or use 'session'/'service'/'key' subcommand$(RESET)") + println(stderr, "$(RED)Error: Provide source_file or use 'session'/'service'/'languages'/'key'/'image' subcommand$(RESET)") exit(1) end end +function cmd_image(args) + (public_key, secret_key) = get_api_keys(args["api-key"]) + + if args["list"] + result = api_request("/images", public_key, secret_key) + println(JSON.json(result, 2)) + return + end + + if args["info"] !== nothing + result = api_request("/images/$(args["info"])", public_key, secret_key) + println(JSON.json(result, 2)) + return + end + + if args["delete"] !== nothing + api_request("/images/$(args["delete"])", public_key, secret_key, method="DELETE") + println("$(GREEN)Image deleted: $(args["delete"])$(RESET)") + return + end + + if args["lock"] !== nothing + api_request("/images/$(args["lock"])/lock", public_key, secret_key, method="POST") + println("$(GREEN)Image locked: $(args["lock"])$(RESET)") + return + end + + if args["unlock"] !== nothing + api_request("/images/$(args["unlock"])/unlock", public_key, secret_key, method="POST") + println("$(GREEN)Image unlocked: $(args["unlock"])$(RESET)") + return + end + + if args["publish"] !== nothing + source_type = args["source-type"] + if source_type === nothing + println(stderr, "$(RED)Error: --source-type required (service or snapshot)$(RESET)") + exit(1) + end + payload = Dict("source_type" => source_type, "source_id" => args["publish"]) + if args["name"] !== nothing + payload["name"] = args["name"] + end + result = api_request("/images/publish", public_key, secret_key, method="POST", data=payload) + println("$(GREEN)Image published$(RESET)") + println(JSON.json(result, 2)) + return + end + + if args["visibility"] !== nothing + mode = args["visibility-mode"] + if mode === nothing + println(stderr, "$(RED)Error: --visibility requires MODE (private, unlisted, or public)$(RESET)") + exit(1) + end + payload = Dict("visibility" => mode) + api_request("/images/$(args["visibility"])/visibility", public_key, secret_key, method="POST", data=payload) + println("$(GREEN)Image visibility set to $(mode): $(args["visibility"])$(RESET)") + return + end + + if args["spawn"] !== nothing + payload = Dict() + if args["name"] !== nothing + payload["name"] = args["name"] + end + if args["ports"] !== nothing + ports = [parse(Int, strip(p)) for p in split(args["ports"], ',')] + payload["ports"] = ports + end + result = api_request("/images/$(args["spawn"])/spawn", public_key, secret_key, method="POST", data=payload) + println("$(GREEN)Service spawned from image$(RESET)") + println(JSON.json(result, 2)) + return + end + + if args["clone"] !== nothing + payload = Dict() + if args["name"] !== nothing + payload["name"] = args["name"] + end + result = api_request("/images/$(args["clone"])/clone", public_key, secret_key, method="POST", data=payload) + println("$(GREEN)Image cloned$(RESET)") + println(JSON.json(result, 2)) + return + end + + println(stderr, "$(RED)Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID, --spawn ID, or --clone ID$(RESET)") + exit(1) +end + main() diff --git a/clients/kotlin/sync/src/un.kt b/clients/kotlin/sync/src/un.kt index e711572..998c82a 100644 --- a/clients/kotlin/sync/src/un.kt +++ b/clients/kotlin/sync/src/un.kt @@ -105,7 +105,21 @@ data class Args( var keyExtend: Boolean = false, var envFile: String? = null, var envAction: String? = null, - var envTarget: String? = null + var envTarget: String? = null, + var jsonOutput: Boolean = false, + var imageList: Boolean = false, + var imageInfo: String? = null, + var imageDelete: String? = null, + var imageLock: String? = null, + var imageUnlock: String? = null, + var imagePublish: String? = null, + var imageSourceType: String? = null, + var imageVisibility: String? = null, + var imageVisibilityMode: String? = null, + var imageSpawn: String? = null, + var imageClone: String? = null, + var imageName: String? = null, + var imagePorts: String? = null ) fun main(args: Array) { @@ -115,7 +129,9 @@ fun main(args: Array) { when (parsedArgs.command) { "session" -> cmdSession(parsedArgs) "service" -> cmdService(parsedArgs) + "languages" -> cmdLanguages(parsedArgs) "key" -> cmdKey(parsedArgs) + "image" -> cmdImage(parsedArgs) else -> if (parsedArgs.sourceFile != null) { cmdExecute(parsedArgs) } else { @@ -459,6 +475,113 @@ fun cmdService(args: Args) { exitProcess(1) } +fun cmdLanguages(args: Args) { + val (publicKey, secretKey) = getApiKeys(args.apiKey) + + val result = apiRequest("/languages", "GET", null, publicKey, secretKey) + @Suppress("UNCHECKED_CAST") + val languages = result["languages"] as? List ?: emptyList() + + if (args.jsonOutput) { + println(toJson(languages)) + } else { + for (lang in languages) { + println(lang) + } + } +} + +fun cmdImage(args: Args) { + val (publicKey, secretKey) = getApiKeys(args.apiKey) + + if (args.imageList) { + val result = apiRequest("/images", "GET", null, publicKey, secretKey) + println(toJson(result)) + return + } + + if (args.imageInfo != null) { + val result = apiRequest("/images/${args.imageInfo}", "GET", null, publicKey, secretKey) + println(toJson(result)) + return + } + + if (args.imageDelete != null) { + apiRequest("/images/${args.imageDelete}", "DELETE", null, publicKey, secretKey) + println("${GREEN}Image deleted: ${args.imageDelete}${RESET}") + return + } + + if (args.imageLock != null) { + apiRequest("/images/${args.imageLock}/lock", "POST", null, publicKey, secretKey) + println("${GREEN}Image locked: ${args.imageLock}${RESET}") + return + } + + if (args.imageUnlock != null) { + apiRequest("/images/${args.imageUnlock}/unlock", "POST", null, publicKey, secretKey) + println("${GREEN}Image unlocked: ${args.imageUnlock}${RESET}") + return + } + + if (args.imagePublish != null) { + if (args.imageSourceType == null) { + System.err.println("${RED}Error: --source-type required (service or snapshot)${RESET}") + exitProcess(1) + } + val payload = mutableMapOf( + "source_type" to args.imageSourceType!!, + "source_id" to args.imagePublish!! + ) + if (args.imageName != null) { + payload["name"] = args.imageName!! + } + val result = apiRequest("/images/publish", "POST", payload, publicKey, secretKey) + println("${GREEN}Image published${RESET}") + println(toJson(result)) + return + } + + if (args.imageVisibility != null) { + if (args.imageVisibilityMode == null) { + System.err.println("${RED}Error: --visibility requires MODE (private, unlisted, or public)${RESET}") + exitProcess(1) + } + val payload = mapOf("visibility" to args.imageVisibilityMode!!) + apiRequest("/images/${args.imageVisibility}/visibility", "POST", payload, publicKey, secretKey) + println("${GREEN}Image visibility set to ${args.imageVisibilityMode}: ${args.imageVisibility}${RESET}") + return + } + + if (args.imageSpawn != null) { + val payload = mutableMapOf() + if (args.imageName != null) { + payload["name"] = args.imageName!! + } + if (args.imagePorts != null) { + payload["ports"] = args.imagePorts!!.split(",").map { it.trim().toInt() } + } + val result = apiRequest("/images/${args.imageSpawn}/spawn", "POST", payload, publicKey, secretKey) + println("${GREEN}Service spawned from image${RESET}") + println(toJson(result)) + return + } + + if (args.imageClone != null) { + val payload = mutableMapOf() + if (args.imageName != null) { + payload["name"] = args.imageName!! + } + val result = apiRequest("/images/${args.imageClone}/clone", "POST", payload, publicKey, secretKey) + println("${GREEN}Image cloned${RESET}") + println(toJson(result)) + return + } + + System.err.println("${RED}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${RESET}") + exitProcess(1) +} + fun cmdKey(args: Args) { val (publicKey, secretKey) = getApiKeys(args.apiKey) @@ -936,7 +1059,10 @@ fun parseArgs(args: Array): Args { when (args[i]) { "session" -> result.command = "session" "service" -> result.command = "service" + "languages" -> result.command = "languages" "key" -> result.command = "key" + "image" -> result.command = "image" + "--json" -> result.jsonOutput = true "-k", "--api-key" -> result.apiKey = args[++i] "-n", "--network" -> result.network = args[++i] "-v", "--vcpu" -> result.vcpu = args[++i].toInt() @@ -948,16 +1074,26 @@ fun parseArgs(args: Array): Args { when (result.command) { "session" -> result.sessionList = true "service" -> result.serviceList = true + "image" -> result.imageList = true } } "-s", "--shell" -> result.sessionShell = args[++i] "--kill" -> result.sessionKill = args[++i] - "--name" -> result.serviceName = args[++i] - "--ports" -> result.servicePorts = args[++i] + "--name" -> { + when (result.command) { + "image" -> result.imageName = args[++i] + else -> result.serviceName = args[++i] + } + } + "--ports" -> { + when (result.command) { + "image" -> result.imagePorts = args[++i] + else -> result.servicePorts = args[++i] + } + } "--type" -> result.serviceType = args[++i] "--bootstrap" -> result.serviceBootstrap = args[++i] "--bootstrap-file" -> result.serviceBootstrapFile = args[++i] - "--info" -> result.serviceInfo = args[++i] "--logs" -> result.serviceLogs = args[++i] "--tail" -> result.serviceTail = args[++i] "--freeze" -> result.serviceSleep = args[++i] @@ -970,6 +1106,39 @@ fun parseArgs(args: Array): Args { "--dump-file" -> result.serviceDumpFile = args[++i] "--extend" -> result.keyExtend = true "--env-file" -> result.envFile = args[++i] + "--info" -> { + when (result.command) { + "service" -> result.serviceInfo = args[++i] + "image" -> result.imageInfo = args[++i] + } + } + "--delete" -> { + if (result.command == "image") result.imageDelete = args[++i] + } + "--lock" -> { + if (result.command == "image") result.imageLock = args[++i] + } + "--unlock" -> { + if (result.command == "image") result.imageUnlock = args[++i] + } + "--publish" -> { + if (result.command == "image") result.imagePublish = args[++i] + } + "--source-type" -> result.imageSourceType = args[++i] + "--visibility" -> { + if (result.command == "image") { + result.imageVisibility = args[++i] + if (i + 1 < args.size && !args[i + 1].startsWith("-")) { + result.imageVisibilityMode = args[++i] + } + } + } + "--spawn" -> { + if (result.command == "image") result.imageSpawn = args[++i] + } + "--clone" -> { + if (result.command == "image") result.imageClone = args[++i] + } "env" -> { if (result.command == "service" && i + 1 < args.size) { result.envAction = args[++i] @@ -997,6 +1166,8 @@ fun printHelp() { Usage: kotlin UnKt [options] kotlin UnKt session [options] kotlin UnKt service [options] + kotlin UnKt image [options] + kotlin UnKt languages [--json] kotlin UnKt key [options] Execute options: @@ -1041,5 +1212,22 @@ Service env commands: Key options: --extend Open browser to extend key + +Image options: + --list List all images + --info ID Get image details + --delete ID Delete an image + --lock ID Lock image to prevent deletion + --unlock ID Unlock image + --publish ID Publish image from service/snapshot (requires --source-type) + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility: private, unlisted, or public + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + +Languages options: + --json Output as JSON array """.trimIndent()) } diff --git a/clients/lisp/sync/src/un.lisp b/clients/lisp/sync/src/un.lisp index 5754293..07321c3 100644 --- a/clients/lisp/sync/src/un.lisp +++ b/clients/lisp/sync/src/un.lisp @@ -486,6 +486,62 @@ (defun key-cmd (extend-flag) (validate-key extend-flag)) +(defun image-cmd (action id name ports source-type visibility-mode) + (let ((api-key (get-api-key))) + (cond + ((string= action "list") + (format t "~a~%" (curl-get api-key "/images"))) + ((string= action "info") + (format t "~a~%" (curl-get api-key (format nil "/images/~a" id)))) + ((string= action "delete") + (curl-delete api-key (format nil "/images/~a" id)) + (format t "~aImage deleted: ~a~a~%" *green* id *reset*)) + ((string= action "lock") + (curl-post api-key (format nil "/images/~a/lock" id) "{}") + (format t "~aImage locked: ~a~a~%" *green* id *reset*)) + ((string= action "unlock") + (curl-post api-key (format nil "/images/~a/unlock" id) "{}") + (format t "~aImage unlocked: ~a~a~%" *green* id *reset*)) + ((string= action "publish") + (if (or (null source-type) (string= source-type "")) + (progn + (format *error-output* "~aError: --source-type required (service or snapshot)~a~%" *red* *reset*) + (uiop:quit 1)) + (let* ((name-json (if name (format nil ",\"name\":\"~a\"" (escape-json name)) "")) + (json (format nil "{\"source_type\":\"~a\",\"source_id\":\"~a\"~a}" source-type id name-json)) + (response (curl-post api-key "/images/publish" json))) + (format t "~aImage published~a~%" *green* *reset*) + (format t "~a~%" response)))) + ((string= action "visibility") + (if (or (null visibility-mode) (string= visibility-mode "")) + (progn + (format *error-output* "~aError: --visibility requires MODE (private, unlisted, or public)~a~%" *red* *reset*) + (uiop:quit 1)) + (let ((json (format nil "{\"visibility\":\"~a\"}" visibility-mode))) + (curl-post api-key (format nil "/images/~a/visibility" id) json) + (format t "~aImage visibility set to ~a: ~a~a~%" *green* visibility-mode id *reset*)))) + ((string= action "spawn") + (let* ((name-json (if name (format nil "\"name\":\"~a\"" (escape-json name)) "")) + (ports-json (if ports (format nil "\"ports\":[~a]" ports) "")) + (content (cond + ((and name ports) (format nil "~a,~a" name-json ports-json)) + (name name-json) + (ports ports-json) + (t ""))) + (json (format nil "{~a}" content)) + (response (curl-post api-key (format nil "/images/~a/spawn" id) json))) + (format t "~aService spawned from image~a~%" *green* *reset*) + (format t "~a~%" response))) + ((string= action "clone") + (let* ((name-json (if name (format nil "\"name\":\"~a\"" (escape-json name)) "")) + (json (format nil "{~a}" name-json)) + (response (curl-post api-key (format nil "/images/~a/clone" id) json))) + (format t "~aImage cloned~a~%" *green* *reset*) + (format t "~a~%" response))) + (t + (format t "~aError: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID~a~%" *red* *reset*) + (uiop:quit 1))))) + (defun parse-input-files (args) "Parse -f flags from args and return list of filenames" (let ((files nil)) @@ -499,6 +555,34 @@ (uiop:quit 1)))))) (nreverse files))) +(defun languages-cmd (json-output) + "List available programming languages" + (let* ((api-key (get-api-key)) + (response (curl-get api-key "/languages")) + (languages-start (search "\"languages\":[" response))) + (if json-output + ;; JSON output - extract and print the languages array + (if languages-start + (let* ((array-start (+ languages-start (length "\"languages\":"))) + (array-end (position #\] response :start array-start)) + (array-content (subseq response array-start (1+ array-end)))) + (format t "~a~%" array-content)) + (format t "[]~%")) + ;; Plain output - one language per line + (if languages-start + (let* ((array-start (+ languages-start (length "\"languages\":["))) + (array-end (position #\] response :start array-start)) + (array-content (subseq response array-start array-end))) + ;; Parse each quoted string + (loop for i = 0 then (+ j 1) + for start = (position #\" array-content :start i) + while start + for end = (position #\" array-content :start (1+ start)) + while end + for j = end + do (format t "~a~%" (subseq array-content (1+ start) end)))) + (format t ""))))) + (defun main () (let ((args (uiop:command-line-arguments))) (if (null args) @@ -506,9 +590,27 @@ (format t "Usage: un.lisp [options] ~%") (format t " un.lisp session [options]~%") (format t " un.lisp service [options]~%") + (format t " un.lisp image [options]~%") + (format t " un.lisp languages [--json]~%") (format t " un.lisp key [--extend]~%") + (format t "~%Image options:~%") + (format t " --list List all images~%") + (format t " --info ID Get image details~%") + (format t " --delete ID Delete an image~%") + (format t " --lock ID Lock image to prevent deletion~%") + (format t " --unlock ID Unlock image~%") + (format t " --publish ID Publish image (requires --source-type)~%") + (format t " --source-type TYPE Source type: service or snapshot~%") + (format t " --visibility ID MODE Set visibility: private, unlisted, public~%") + (format t " --spawn ID Spawn service from image~%") + (format t " --clone ID Clone an image~%") + (format t " --name NAME Name for spawned service or cloned image~%") + (format t " --ports PORTS Ports for spawned service~%") (uiop:quit 1)) (cond + ((string= (first args) "languages") + (let ((json-flag (and (> (length args) 1) (string= (second args) "--json")))) + (languages-cmd json-flag))) ((string= (first args) "session") (cond ((and (> (length args) 1) (string= (second args) "--list")) @@ -617,6 +719,60 @@ ((string= (first args) "key") (let ((extend-flag (and (> (length args) 1) (string= (second args) "--extend")))) (key-cmd extend-flag))) + ((string= (first args) "image") + (cond + ((and (> (length args) 1) (string= (second args) "--list")) + (image-cmd "list" nil nil nil nil nil)) + ((and (> (length args) 2) (string= (second args) "--info")) + (image-cmd "info" (third args) nil nil nil nil)) + ((and (> (length args) 2) (string= (second args) "--delete")) + (image-cmd "delete" (third args) nil nil nil nil)) + ((and (> (length args) 2) (string= (second args) "--lock")) + (image-cmd "lock" (third args) nil nil nil nil)) + ((and (> (length args) 2) (string= (second args) "--unlock")) + (image-cmd "unlock" (third args) nil nil nil nil)) + ((and (> (length args) 2) (string= (second args) "--publish")) + ;; --publish ID --source-type TYPE [--name NAME] + (let* ((source-id (third args)) + (rest-args (nthcdr 3 args)) + (source-type nil) + (name nil)) + (loop for i from 0 below (1- (length rest-args)) + do (let ((opt (nth i rest-args)) + (val (nth (1+ i) rest-args))) + (cond + ((string= opt "--source-type") (setf source-type val)) + ((string= opt "--name") (setf name val))))) + (image-cmd "publish" source-id name nil source-type nil))) + ((and (> (length args) 3) (string= (second args) "--visibility")) + ;; --visibility ID MODE + (image-cmd "visibility" (third args) nil nil nil (fourth args))) + ((and (> (length args) 2) (string= (second args) "--spawn")) + ;; --spawn ID [--name NAME] [--ports PORTS] + (let* ((image-id (third args)) + (rest-args (nthcdr 3 args)) + (name nil) + (ports nil)) + (loop for i from 0 below (1- (length rest-args)) + do (let ((opt (nth i rest-args)) + (val (nth (1+ i) rest-args))) + (cond + ((string= opt "--name") (setf name val)) + ((string= opt "--ports") (setf ports val))))) + (image-cmd "spawn" image-id name ports nil nil))) + ((and (> (length args) 2) (string= (second args) "--clone")) + ;; --clone ID [--name NAME] + (let* ((image-id (third args)) + (rest-args (nthcdr 3 args)) + (name nil)) + (loop for i from 0 below (1- (length rest-args)) + do (let ((opt (nth i rest-args)) + (val (nth (1+ i) rest-args))) + (when (string= opt "--name") (setf name val)))) + (image-cmd "clone" image-id name nil nil nil))) + (t + (format t "~aError: Invalid image command~a~%" *red* *reset*) + (uiop:quit 1)))) (t (execute-cmd (first args))))))) diff --git a/clients/lua/sync/src/un.lua b/clients/lua/sync/src/un.lua index e6fa347..af9ffb9 100644 --- a/clients/lua/sync/src/un.lua +++ b/clients/lua/sync/src/un.lua @@ -189,12 +189,214 @@ function Un.detect_language(filename) return map[ext] or error("Unknown file type") end +-- Image API functions +function Un.image_list(opts) + opts = opts or {} + return Un.api_request("GET", "/images", nil, opts) +end + +function Un.image_get(image_id, opts) + opts = opts or {} + return Un.api_request("GET", "/images/" .. image_id, nil, opts) +end + +function Un.image_delete(image_id, opts) + opts = opts or {} + return Un.api_request("DELETE", "/images/" .. image_id, nil, opts) +end + +function Un.image_lock(image_id, opts) + opts = opts or {} + return Un.api_request("POST", "/images/" .. image_id .. "/lock", {}, opts) +end + +function Un.image_unlock(image_id, opts) + opts = opts or {} + return Un.api_request("POST", "/images/" .. image_id .. "/unlock", {}, opts) +end + +function Un.image_publish(source_id, source_type, name, opts) + opts = opts or {} + local body = {source_type = source_type, source_id = source_id} + if name then body.name = name end + return Un.api_request("POST", "/images/publish", body, opts) +end + +function Un.image_visibility(image_id, visibility, opts) + opts = opts or {} + return Un.api_request("POST", "/images/" .. image_id .. "/visibility", {visibility = visibility}, opts) +end + +function Un.image_spawn(image_id, name, ports, opts) + opts = opts or {} + local body = {} + if name then body.name = name end + if ports then body.ports = ports end + return Un.api_request("POST", "/images/" .. image_id .. "/spawn", body, opts) +end + +function Un.image_clone(image_id, name, opts) + opts = opts or {} + local body = {} + if name then body.name = name end + return Un.api_request("POST", "/images/" .. image_id .. "/clone", body, opts) +end + -- CLI if arg and arg[1] then - local result = Un.run(arg[1]) - if result.stdout then print(result.stdout) end - if result.stderr then io.stderr:write(result.stderr) end - os.exit(result.exit_code or 0) + if arg[1] == "languages" then + -- Languages command + local json_output = arg[2] == "--json" + local langs = Un.languages() + + if json_output then + print(json.encode(langs)) + else + for _, lang in ipairs(langs) do + print(lang) + end + end + os.exit(0) + elseif arg[1] == "image" then + -- Image command + local i = 2 + local action = nil + local image_id = nil + local name = nil + local ports = nil + local source_type = nil + local visibility_mode = nil + + while i <= #arg do + if arg[i] == "--list" or arg[i] == "-l" then + action = "list" + elseif arg[i] == "--info" then + action = "info" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--delete" then + action = "delete" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--lock" then + action = "lock" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--unlock" then + action = "unlock" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--publish" then + action = "publish" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--source-type" then + i = i + 1 + source_type = arg[i] + elseif arg[i] == "--visibility" then + action = "visibility" + i = i + 1 + image_id = arg[i] + if i + 1 <= #arg and not arg[i + 1]:match("^%-") then + i = i + 1 + visibility_mode = arg[i] + end + elseif arg[i] == "--spawn" then + action = "spawn" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--clone" then + action = "clone" + i = i + 1 + image_id = arg[i] + elseif arg[i] == "--name" then + i = i + 1 + name = arg[i] + elseif arg[i] == "--ports" then + i = i + 1 + ports = {} + for p in arg[i]:gmatch("[^,]+") do + table.insert(ports, tonumber(p)) + end + end + i = i + 1 + end + + if action == "list" then + local result = Un.image_list() + print(json.encode(result)) + elseif action == "info" then + local result = Un.image_get(image_id) + print(json.encode(result)) + elseif action == "delete" then + Un.image_delete(image_id) + print("Image deleted: " .. image_id) + elseif action == "lock" then + Un.image_lock(image_id) + print("Image locked: " .. image_id) + elseif action == "unlock" then + Un.image_unlock(image_id) + print("Image unlocked: " .. image_id) + elseif action == "publish" then + if not source_type then + io.stderr:write("Error: --source-type required (service or snapshot)\n") + os.exit(1) + end + local result = Un.image_publish(image_id, source_type, name) + print("Image published") + print(json.encode(result)) + elseif action == "visibility" then + if not visibility_mode then + io.stderr:write("Error: --visibility requires MODE (private, unlisted, or public)\n") + os.exit(1) + end + Un.image_visibility(image_id, visibility_mode) + print("Image visibility set to " .. visibility_mode .. ": " .. image_id) + elseif action == "spawn" then + local result = Un.image_spawn(image_id, name, ports) + print("Service spawned from image") + print(json.encode(result)) + elseif action == "clone" then + local result = Un.image_clone(image_id, name) + print("Image cloned") + print(json.encode(result)) + else + io.stderr:write("Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID\n") + os.exit(1) + end + os.exit(0) + elseif arg[1] == "--help" or arg[1] == "-h" then + print("Usage: lua un.lua [options] ") + print(" lua un.lua languages [--json]") + print(" lua un.lua image [options]") + print("") + print("Commands:") + print(" languages [--json] List available programming languages") + print(" image [options] Manage images") + print("") + print("Languages options:") + print(" --json Output as JSON array") + print("") + print("Image options:") + print(" --list List all images") + print(" --info ID Get image details") + print(" --delete ID Delete an image") + print(" --lock ID Lock image to prevent deletion") + print(" --unlock ID Unlock image") + print(" --publish ID Publish image (requires --source-type)") + print(" --source-type TYPE Source type: service or snapshot") + print(" --visibility ID MODE Set visibility: private, unlisted, public") + print(" --spawn ID Spawn service from image") + print(" --clone ID Clone an image") + print(" --name NAME Name for spawned service or cloned image") + print(" --ports PORTS Ports for spawned service") + os.exit(0) + else + local result = Un.run(arg[1]) + if result.stdout then print(result.stdout) end + if result.stderr then io.stderr:write(result.stderr) end + os.exit(result.exit_code or 0) + end end return Un diff --git a/clients/nim/sync/src/un.nim b/clients/nim/sync/src/un.nim index d874dc7..c2248fd 100644 --- a/clients/nim/sync/src/un.nim +++ b/clients/nim/sync/src/un.nim @@ -564,6 +564,149 @@ proc cmdKey(extend: bool, publicKey: string, secretKey: string) = if errEnd > errStart: echo "Error: " & response[errStart..= 0: + var bracketStart = response.find("[", langStart) + if bracketStart >= 0: + var depth = 1 + var bracketEnd = bracketStart + 1 + while bracketEnd < response.len and depth > 0: + if response[bracketEnd] == '[': inc depth + elif response[bracketEnd] == ']': dec depth + inc bracketEnd + if bracketEnd <= response.len: + echo response[bracketStart..= 0: + var bracketStart = response.find("[", langStart) + if bracketStart >= 0: + var pos = bracketStart + 1 + while pos < response.len: + # Skip whitespace + while pos < response.len and response[pos] in {' ', '\n', '\r', '\t', ','}: inc pos + if pos >= response.len or response[pos] == ']': break + # Find quoted string + if response[pos] == '"': + let start = pos + 1 + var endPos = start + while endPos < response.len and response[endPos] != '"': + inc endPos + if endPos > start: + echo response[start..") stderr.writeLine(" un.nim session [options]") stderr.writeLine(" un.nim service [options]") + stderr.writeLine(" un.nim image [options]") stderr.writeLine(" un.nim service env [options]") + stderr.writeLine(" un.nim languages [--json]") stderr.writeLine(" un.nim key [options]") stderr.writeLine("") + stderr.writeLine("Languages options:") + stderr.writeLine(" --json Output as JSON array") + stderr.writeLine("") + stderr.writeLine("Image options:") + stderr.writeLine(" --list, -l List all images") + stderr.writeLine(" --info ID Get image details") + stderr.writeLine(" --delete ID Delete an image") + stderr.writeLine(" --lock ID Lock image to prevent deletion") + stderr.writeLine(" --unlock ID Unlock image") + stderr.writeLine(" --publish ID Publish image from service/snapshot") + stderr.writeLine(" --source-type TYPE Source type: service or snapshot") + stderr.writeLine(" --visibility ID MODE Set visibility: private, unlisted, or public") + stderr.writeLine(" --spawn ID Spawn new service from image") + stderr.writeLine(" --clone ID Clone an image") + stderr.writeLine(" --name NAME Name for spawned service or cloned image") + stderr.writeLine(" --ports PORTS Ports for spawned service") + stderr.writeLine("") stderr.writeLine("Service env commands:") stderr.writeLine(" env status Show vault status") stderr.writeLine(" env set Set vault (-e KEY=VALUE or --env-file FILE)") @@ -592,6 +754,46 @@ proc main() = stderr.writeLine(" --env-file FILE Load env vars from file (for vault)") quit(1) + if args[0] == "languages": + var jsonOutput = false + var i = 1 + while i < args.len: + case args[i] + of "--json": jsonOutput = true + of "-k": publicKey = args[i+1]; inc i + else: discard + inc i + cmdLanguages(jsonOutput, publicKey, secretKey) + return + + if args[0] == "image": + var list = false + var infoId, deleteId, lockId, unlockId, publishId, sourceType = "" + var visibilityId, visibilityMode, spawnId, cloneId, name, ports = "" + var i = 1 + while i < args.len: + case args[i] + of "--list", "-l": list = true + of "--info": infoId = args[i+1]; inc i + of "--delete": deleteId = args[i+1]; inc i + of "--lock": lockId = args[i+1]; inc i + of "--unlock": unlockId = args[i+1]; inc i + of "--publish": publishId = args[i+1]; inc i + of "--source-type": sourceType = args[i+1]; inc i + of "--visibility": + visibilityId = args[i+1] + visibilityMode = args[i+2] + inc i, 2 + of "--spawn": spawnId = args[i+1]; inc i + of "--clone": cloneId = args[i+1]; inc i + of "--name": name = args[i+1]; inc i + of "--ports": ports = args[i+1]; inc i + of "-k": publicKey = args[i+1]; inc i + else: discard + inc i + cmdImage(list, infoId, deleteId, lockId, unlockId, publishId, sourceType, visibilityId, visibilityMode, spawnId, cloneId, name, ports, publicKey, secretKey) + return + if args[0] == "key": var extend = false var i = 1 diff --git a/clients/objective-c/sync/src/un.m b/clients/objective-c/sync/src/un.m index 00c345a..7874a58 100644 --- a/clients/objective-c/sync/src/un.m +++ b/clients/objective-c/sync/src/un.m @@ -1491,12 +1491,190 @@ void cmdKey(NSArray* args) { printf("Public Key: %s\n", [publicKey UTF8String]); } +void cmdImage(NSArray* args) { + NSString* publicKey, *secretKey; + UNGetApiKeysCLI(&publicKey, &secretKey); + + BOOL listMode = NO; + NSString* infoId = nil; + NSString* deleteId = nil; + NSString* lockId = nil; + NSString* unlockId = nil; + NSString* publishId = nil; + NSString* sourceType = nil; + NSString* visibilityId = nil; + NSString* visibilityMode = nil; + NSString* spawnId = nil; + NSString* cloneId = nil; + NSString* name = nil; + NSString* ports = nil; + + for (NSUInteger i = 0; i < [args count]; i++) { + NSString* arg = args[i]; + if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) { + listMode = YES; + } else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) { + infoId = args[++i]; + } else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) { + deleteId = args[++i]; + } else if ([arg isEqualToString:@"--lock"] && i + 1 < [args count]) { + lockId = args[++i]; + } else if ([arg isEqualToString:@"--unlock"] && i + 1 < [args count]) { + unlockId = args[++i]; + } else if ([arg isEqualToString:@"--publish"] && i + 1 < [args count]) { + publishId = args[++i]; + } else if ([arg isEqualToString:@"--source-type"] && i + 1 < [args count]) { + sourceType = args[++i]; + } else if ([arg isEqualToString:@"--visibility"] && i + 2 < [args count]) { + visibilityId = args[++i]; + visibilityMode = args[++i]; + } else if ([arg isEqualToString:@"--spawn"] && i + 1 < [args count]) { + spawnId = args[++i]; + } else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) { + cloneId = args[++i]; + } else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) { + name = args[++i]; + } else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) { + ports = args[++i]; + } + } + + if (listMode) { + NSDictionary* result = apiRequestCLI(@"/images", @"GET", nil, publicKey, secretKey); + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + printf("%s\n", [jsonString UTF8String]); + return; + } + + if (infoId) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@", infoId]; + NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey); + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + printf("%s\n", [jsonString UTF8String]); + return; + } + + if (deleteId) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@", deleteId]; + apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey); + printf("%sImage deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]); + return; + } + + if (lockId) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@/lock", lockId]; + apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey); + printf("%sImage locked: %s%s\n", [GREEN UTF8String], [lockId UTF8String], [RESET UTF8String]); + return; + } + + if (unlockId) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@/unlock", unlockId]; + apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey); + printf("%sImage unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]); + return; + } + + if (publishId) { + if (!sourceType) { + fprintf(stderr, "%sError: --publish requires --source-type (service or snapshot)%s\n", + [RED UTF8String], [RESET UTF8String]); + exit(1); + } + NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{ + @"source_type": sourceType, + @"source_id": publishId + }]; + if (name) payload[@"name"] = name; + + NSDictionary* result = apiRequestCLI(@"/images/publish", @"POST", payload, publicKey, secretKey); + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + printf("%sImage published%s\n", [GREEN UTF8String], [RESET UTF8String]); + printf("%s\n", [jsonString UTF8String]); + return; + } + + if (visibilityId && visibilityMode) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@/visibility", visibilityId]; + NSDictionary* payload = @{@"visibility": visibilityMode}; + apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey); + printf("%sImage visibility set to %s: %s%s\n", [GREEN UTF8String], [visibilityMode UTF8String], [visibilityId UTF8String], [RESET UTF8String]); + return; + } + + if (spawnId) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@/spawn", spawnId]; + NSMutableDictionary* payload = [NSMutableDictionary dictionary]; + if (name) payload[@"name"] = name; + if (ports) { + NSArray* portStrings = [ports componentsSeparatedByString:@","]; + NSMutableArray* portNumbers = [NSMutableArray array]; + for (NSString* p in portStrings) { + [portNumbers addObject:@([p intValue])]; + } + payload[@"ports"] = portNumbers; + } + + NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey); + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + printf("%sService spawned from image%s\n", [GREEN UTF8String], [RESET UTF8String]); + printf("%s\n", [jsonString UTF8String]); + return; + } + + if (cloneId) { + NSString* endpoint = [NSString stringWithFormat:@"/images/%@/clone", cloneId]; + NSMutableDictionary* payload = [NSMutableDictionary dictionary]; + if (name) payload[@"name"] = name; + + NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey); + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + printf("%sImage cloned%s\n", [GREEN UTF8String], [RESET UTF8String]); + printf("%s\n", [jsonString UTF8String]); + return; + } + + fprintf(stderr, "%sError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone%s\n", + [RED UTF8String], [RESET UTF8String]); + exit(1); +} + +void cmdLanguages(BOOL jsonOutput) { + NSString* publicKey, *secretKey; + UNGetApiKeysCLI(&publicKey, &secretKey); + + NSDictionary* result = apiRequestCLI(@"/languages", @"GET", nil, publicKey, secretKey); + NSArray* languages = result[@"languages"]; + + if (jsonOutput) { + // Output as JSON array + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:languages options:0 error:&error]; + if (!error && jsonData) { + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + printf("%s\n", [jsonString UTF8String]); + } + } else { + // Output one language per line + for (NSString* lang in languages) { + printf("%s\n", [lang UTF8String]); + } + } +} + void showHelp(void) { printf("unsandbox - Execute code in secure sandboxes\n\n"); printf("Usage:\n"); printf(" un.m [options] \n"); printf(" un.m session [options]\n"); printf(" un.m service [options]\n"); + printf(" un.m image [options]\n"); + printf(" un.m languages [--json]\n"); printf(" un.m key [options]\n\n"); printf("Execute options:\n"); printf(" -e KEY=VALUE Environment variable (multiple allowed)\n"); @@ -1519,6 +1697,21 @@ void showHelp(void) { printf(" --name NAME Create service with name\n"); printf(" --ports PORTS Comma-separated ports\n"); printf(" --bootstrap CMD Bootstrap command\n\n"); + printf("Image options:\n"); + printf(" --list, -l List all images\n"); + printf(" --info ID Get image details\n"); + printf(" --delete ID Delete an image\n"); + printf(" --lock ID Lock image to prevent deletion\n"); + printf(" --unlock ID Unlock image\n"); + printf(" --publish ID Publish image from service/snapshot\n"); + printf(" --source-type TYPE Source type: service or snapshot\n"); + printf(" --visibility ID MODE Set visibility: private, unlisted, or public\n"); + printf(" --spawn ID Spawn new service from image\n"); + printf(" --clone ID Clone an image\n"); + printf(" --name NAME Name for spawned service or cloned image\n"); + printf(" --ports PORTS Ports for spawned service\n\n"); + printf("Languages options:\n"); + printf(" --json Output as JSON array\n\n"); printf("Library Usage:\n"); printf(" #import \"un.m\"\n"); printf(" UNClient *client = [[UNClient alloc] init];\n"); @@ -1554,6 +1747,16 @@ int main(int argc, const char* argv[]) { cmdSession([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); } else if ([firstArg isEqualToString:@"service"]) { cmdService([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); + } else if ([firstArg isEqualToString:@"image"]) { + cmdImage([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); + } else if ([firstArg isEqualToString:@"languages"]) { + BOOL jsonOutput = NO; + for (NSUInteger i = 1; i < [args count]; i++) { + if ([args[i] isEqualToString:@"--json"]) { + jsonOutput = YES; + } + } + cmdLanguages(jsonOutput); } else if ([firstArg isEqualToString:@"key"]) { cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); } else { diff --git a/clients/ocaml/sync/src/un.ml b/clients/ocaml/sync/src/un.ml index 51b8b6b..da3d525 100755 --- a/clients/ocaml/sync/src/un.ml +++ b/clients/ocaml/sync/src/un.ml @@ -1300,6 +1300,117 @@ let rec parse_input_files acc = function CLI Entry Point ============================================================================ *) +(* Languages command *) +let languages_command json_output = + let response = languages () in + if json_output then begin + (* Extract languages array and output as JSON *) + let pattern = "\"languages\":\\s*\\[\\([^]]*\\)\\]" in + let regex = Str.regexp pattern in + try + let _ = Str.search_forward regex response 0 in + let langs_str = Str.matched_group 1 response in + Printf.printf "[%s]\n" langs_str + with Not_found -> + Printf.printf "[]\n" + end else begin + (* Extract each language and print one per line *) + let pattern = "\"\\([a-zA-Z0-9_+-]+\\)\"" in + let regex = Str.regexp pattern in + (* Find the languages array first *) + let langs_pattern = "\"languages\":\\s*\\[\\([^]]*\\)\\]" in + let langs_regex = Str.regexp langs_pattern in + try + let _ = Str.search_forward langs_regex response 0 in + let langs_str = Str.matched_group 1 response in + let rec extract_langs pos = + try + let _ = Str.search_forward regex langs_str pos in + let lang = Str.matched_group 1 langs_str in + Printf.printf "%s\n" lang; + extract_langs (Str.match_end ()) + with Not_found -> () + in + extract_langs 0 + with Not_found -> () + end + +(* Image command *) +let image_command args = + let api_key = get_api_key () in + let rec parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports = function + | [] -> + if list_mode then begin + let response = curl_get api_key "/images" in + Printf.printf "%s\n" response + end + else if info_id <> "" then begin + let response = curl_get api_key (Printf.sprintf "/images/%s" info_id) in + Printf.printf "%s\n" response + end + else if delete_id <> "" then begin + let _ = curl_delete api_key (Printf.sprintf "/images/%s" delete_id) in + Printf.printf "%sImage deleted: %s%s\n" green delete_id reset + end + else if lock_id <> "" then begin + let _ = curl_post api_key (Printf.sprintf "/images/%s/lock" lock_id) "{}" in + Printf.printf "%sImage locked: %s%s\n" green lock_id reset + end + else if unlock_id <> "" then begin + let _ = curl_post api_key (Printf.sprintf "/images/%s/unlock" unlock_id) "{}" in + Printf.printf "%sImage unlocked: %s%s\n" green unlock_id reset + end + else if publish_id <> "" then begin + if source_type = "" then begin + Printf.fprintf stderr "%sError: --publish requires --source-type (service or snapshot)%s\n" red reset; + exit 1 + end; + let name_json = if name <> "" then Printf.sprintf ",\"name\":\"%s\"" name else "" in + let json = Printf.sprintf "{\"source_type\":\"%s\",\"source_id\":\"%s\"%s}" source_type publish_id name_json in + let response = curl_post api_key "/images/publish" json in + Printf.printf "%sImage published%s\n" green reset; + Printf.printf "%s\n" response + end + else if visibility_id <> "" && visibility_mode <> "" then begin + let json = Printf.sprintf "{\"visibility\":\"%s\"}" visibility_mode in + let _ = curl_post api_key (Printf.sprintf "/images/%s/visibility" visibility_id) json in + Printf.printf "%sImage visibility set to %s: %s%s\n" green visibility_mode visibility_id reset + end + else if spawn_id <> "" then begin + let name_json = if name <> "" then Printf.sprintf "\"name\":\"%s\"" name else "" in + let ports_json = if ports <> "" then Printf.sprintf "%s\"ports\":[%s]" (if name <> "" then "," else "") ports else "" in + let json = Printf.sprintf "{%s%s}" name_json ports_json in + let response = curl_post api_key (Printf.sprintf "/images/%s/spawn" spawn_id) json in + Printf.printf "%sService spawned from image%s\n" green reset; + Printf.printf "%s\n" response + end + else if clone_id <> "" then begin + let name_json = if name <> "" then Printf.sprintf "{\"name\":\"%s\"}" name else "{}" in + let response = curl_post api_key (Printf.sprintf "/images/%s/clone" clone_id) name_json in + Printf.printf "%sImage cloned%s\n" green reset; + Printf.printf "%s\n" response + end + else begin + Printf.fprintf stderr "%sError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone%s\n" red reset; + exit 1 + end + | "--list" :: rest -> parse_args true info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "-l" :: rest -> parse_args true info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "--info" :: id :: rest -> parse_args list_mode id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "--delete" :: id :: rest -> parse_args list_mode info_id id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "--lock" :: id :: rest -> parse_args list_mode info_id delete_id id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "--unlock" :: id :: rest -> parse_args list_mode info_id delete_id lock_id id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "--publish" :: id :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + | "--source-type" :: t :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id t visibility_id visibility_mode spawn_id clone_id name ports rest + | "--visibility" :: id :: mode :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type id mode spawn_id clone_id name ports rest + | "--spawn" :: id :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode id clone_id name ports rest + | "--clone" :: id :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id id name ports rest + | "--name" :: n :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id n ports rest + | "--ports" :: p :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name p rest + | _ :: rest -> parse_args list_mode info_id delete_id lock_id unlock_id publish_id source_type visibility_id visibility_mode spawn_id clone_id name ports rest + in + parse_args false "" "" "" "" "" "" "" "" "" "" "" "" args + let () = Random.self_init (); let args = Array.to_list Sys.argv in @@ -1308,11 +1419,22 @@ let () = Printf.printf "Usage: un.ml [options] \n"; Printf.printf " un.ml session [options]\n"; Printf.printf " un.ml service [options]\n"; + Printf.printf " un.ml image [options]\n"; Printf.printf " un.ml service env \n"; + Printf.printf " un.ml languages [--json]\n"; Printf.printf " un.ml key [--extend]\n\n"; Printf.printf "Service options: --name, --ports, --bootstrap, --bootstrap-file, -e KEY=VALUE, --env-file FILE\n"; Printf.printf "Service env commands: status, set, export, delete\n"; + Printf.printf "Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,\n"; + Printf.printf " --publish ID --source-type TYPE, --visibility ID MODE,\n"; + Printf.printf " --spawn ID, --clone ID, --name NAME, --ports PORTS\n"; + Printf.printf "Languages options: --json (output as JSON array)\n"; exit 1 + | "languages" :: rest -> + let json_output = List.mem "--json" rest in + languages_command json_output + | "image" :: rest -> + image_command rest | "key" :: rest -> let extend = List.mem "--extend" rest in key_command extend diff --git a/clients/perl/sync/src/un.pl b/clients/perl/sync/src/un.pl index 459a64d..a874c8e 100644 --- a/clients/perl/sync/src/un.pl +++ b/clients/perl/sync/src/un.pl @@ -913,6 +913,113 @@ sub cmd_key { validate_key($public_key, $secret_key, $options->{extend}); } +sub cmd_languages { + my ($options) = @_; + my ($public_key, $secret_key) = get_api_key($options->{api_key}); + + my $result = api_request('/languages', 'GET', undef, $public_key, $secret_key); + my $languages_list = $result->{languages} || []; + + if ($options->{json}) { + # Output as JSON array + print encode_json($languages_list); + print "\n"; + } else { + # Output one language per line + foreach my $lang (@$languages_list) { + print "$lang\n"; + } + } +} + +sub cmd_image { + my ($options) = @_; + my ($public_key, $secret_key) = get_api_key($options->{api_key}); + + if ($options->{list}) { + my $result = api_request('/images', 'GET', undef, $public_key, $secret_key); + print encode_json($result); + print "\n"; + return; + } + + if ($options->{info}) { + my $result = api_request("/images/$options->{info}", 'GET', undef, $public_key, $secret_key); + print encode_json($result); + print "\n"; + return; + } + + if ($options->{delete}) { + api_request("/images/$options->{delete}", 'DELETE', undef, $public_key, $secret_key); + print "${GREEN}Image deleted: $options->{delete}${RESET}\n"; + return; + } + + if ($options->{lock}) { + api_request("/images/$options->{lock}/lock", 'POST', undef, $public_key, $secret_key); + print "${GREEN}Image locked: $options->{lock}${RESET}\n"; + return; + } + + if ($options->{unlock}) { + api_request("/images/$options->{unlock}/unlock", 'POST', undef, $public_key, $secret_key); + print "${GREEN}Image unlocked: $options->{unlock}${RESET}\n"; + return; + } + + if ($options->{publish}) { + unless ($options->{source_type}) { + print STDERR "${RED}Error: --publish requires --source-type (service or snapshot)${RESET}\n"; + exit 1; + } + my $payload = { + source_type => $options->{source_type}, + source_id => $options->{publish} + }; + $payload->{name} = $options->{name} if $options->{name}; + my $result = api_request('/images/publish', 'POST', $payload, $public_key, $secret_key); + print "${GREEN}Image published${RESET}\n"; + print encode_json($result); + print "\n"; + return; + } + + if ($options->{visibility_id} && $options->{visibility_mode}) { + my $payload = { visibility => $options->{visibility_mode} }; + api_request("/images/$options->{visibility_id}/visibility", 'POST', $payload, $public_key, $secret_key); + print "${GREEN}Image visibility set to $options->{visibility_mode}: $options->{visibility_id}${RESET}\n"; + return; + } + + if ($options->{spawn}) { + my $payload = {}; + $payload->{name} = $options->{name} if $options->{name}; + if ($options->{ports}) { + my @ports = map { int($_) } split(',', $options->{ports}); + $payload->{ports} = \@ports; + } + my $result = api_request("/images/$options->{spawn}/spawn", 'POST', $payload, $public_key, $secret_key); + print "${GREEN}Service spawned from image${RESET}\n"; + print encode_json($result); + print "\n"; + return; + } + + if ($options->{clone}) { + my $payload = {}; + $payload->{name} = $options->{name} if $options->{name}; + my $result = api_request("/images/$options->{clone}/clone", 'POST', $payload, $public_key, $secret_key); + print "${GREEN}Image cloned${RESET}\n"; + print encode_json($result); + print "\n"; + return; + } + + print STDERR "${RED}Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone${RESET}\n"; + exit 1; +} + sub main { my %options = ( command => undef, @@ -951,13 +1058,24 @@ sub main { extend => 0, env_file => undef, env_action => undef, - env_target => undef + env_target => undef, + json => 0, + # Image options + delete => undef, + lock => undef, + unlock => undef, + publish => undef, + source_type => undef, + visibility_id => undef, + visibility_mode => undef, + spawn => undef, + clone => undef ); for (my $i = 0; $i < @ARGV; $i++) { my $arg = $ARGV[$i]; - if ($arg eq 'session' || $arg eq 'service' || $arg eq 'key') { + if ($arg eq 'session' || $arg eq 'service' || $arg eq 'key' || $arg eq 'languages' || $arg eq 'image') { $options{command} = $arg; } elsif ($arg eq '-e') { push @{$options{env}}, $ARGV[++$i]; @@ -1033,6 +1151,25 @@ sub main { $options{dump_file} = $ARGV[++$i]; } elsif ($arg eq '--extend') { $options{extend} = 1; + } elsif ($arg eq '--json') { + $options{json} = 1; + } elsif ($arg eq '--delete') { + $options{delete} = $ARGV[++$i]; + } elsif ($arg eq '--lock') { + $options{lock} = $ARGV[++$i]; + } elsif ($arg eq '--unlock') { + $options{unlock} = $ARGV[++$i]; + } elsif ($arg eq '--publish') { + $options{publish} = $ARGV[++$i]; + } elsif ($arg eq '--source-type') { + $options{source_type} = $ARGV[++$i]; + } elsif ($arg eq '--visibility') { + $options{visibility_id} = $ARGV[++$i]; + $options{visibility_mode} = $ARGV[++$i] if defined $ARGV[$i + 1]; + } elsif ($arg eq '--spawn') { + $options{spawn} = $ARGV[++$i]; + } elsif ($arg eq '--clone') { + $options{clone} = $ARGV[++$i]; } elsif ($arg =~ /^-/) { print STDERR "${RED}Unknown option: $arg${RESET}\n"; exit 1; @@ -1051,6 +1188,10 @@ sub main { } else { cmd_service(\%options); } + } elsif ($options{command} && $options{command} eq 'languages') { + cmd_languages(\%options); + } elsif ($options{command} && $options{command} eq 'image') { + cmd_image(\%options); } elsif ($options{command} && $options{command} eq 'key') { cmd_key(\%options); } elsif ($options{source_file}) { @@ -1063,8 +1204,27 @@ Usage: $0 [options] $0 session [options] $0 service [options] + $0 image [options] + $0 languages [--json] $0 key [options] +Languages options: + --json Output as JSON array + +Image options: + --list, -l List all images + --info ID Get image details + --delete ID Delete an image + --lock ID Lock image to prevent deletion + --unlock ID Unlock image + --publish ID Publish image from service/snapshot + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility: private, unlisted, or public + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + Execute options: -e KEY=VALUE Environment variable (multiple allowed) -f FILE Input file (multiple allowed) diff --git a/clients/php/sync/src/un.php b/clients/php/sync/src/un.php index a47945e..9238378 100644 --- a/clients/php/sync/src/un.php +++ b/clients/php/sync/src/un.php @@ -1760,9 +1760,15 @@ class Unsandbox { case 'snapshot': $this->cliHandleSnapshot(array_slice($args, 1), $opts); break; + case 'image': + $this->cliHandleImage(array_slice($args, 1), $opts); + break; case 'key': $this->cliHandleKey($opts); break; + case 'languages': + $this->cliHandleLanguages(array_slice($args, 1), $opts); + break; case '-h': case '--help': case 'help': @@ -2578,6 +2584,226 @@ class Unsandbox { return $opts; } + /** + * Handle image subcommand. + * + * @param array $args Command arguments + * @param array $opts Global options + */ + private function cliHandleImage(array $args, array $opts): void { + $imageOpts = $this->cliParseImageOptions($args); + + if ($imageOpts['list']) { + $images = $this->listImages(); + $this->cliPrintImageList($images); + return; + } + + if ($imageOpts['info']) { + $image = $this->getImage($imageOpts['info']); + $this->cliPrintImageInfo($image); + return; + } + + if ($imageOpts['delete']) { + if (!$opts['yes']) { + fwrite(STDERR, "Warning: This will permanently delete the image. Use -y to confirm.\n"); + exit(2); + } + $result = $this->deleteImage($imageOpts['delete']); + echo "Image deleted: " . $imageOpts['delete'] . "\n"; + return; + } + + if ($imageOpts['lock']) { + $result = $this->lockImage($imageOpts['lock']); + echo "Image locked: " . $imageOpts['lock'] . "\n"; + return; + } + + if ($imageOpts['unlock']) { + $result = $this->unlockImage($imageOpts['unlock']); + echo "Image unlocked: " . $imageOpts['unlock'] . "\n"; + return; + } + + if ($imageOpts['publish']) { + if (empty($imageOpts['sourceType'])) { + $this->cliError("--source-type required for --publish"); + exit(2); + } + $result = $this->imagePublish($imageOpts['sourceType'], $imageOpts['publish'], $imageOpts['name']); + $imageId = $result['image_id'] ?? $result['id'] ?? ''; + echo "Image published: {$imageId}\n"; + return; + } + + if ($imageOpts['visibility'] && $imageOpts['visibilityMode']) { + if (!in_array($imageOpts['visibilityMode'], ['private', 'unlisted', 'public'])) { + $this->cliError("visibility must be private, unlisted, or public"); + exit(2); + } + $result = $this->setImageVisibility($imageOpts['visibility'], $imageOpts['visibilityMode']); + echo "Image {$imageOpts['visibility']} visibility set to {$imageOpts['visibilityMode']}\n"; + return; + } + + if ($imageOpts['spawn']) { + if (empty($imageOpts['name'])) { + $this->cliError("--name required for --spawn"); + exit(2); + } + $ports = null; + if (!empty($imageOpts['ports'])) { + $ports = array_map('intval', explode(',', $imageOpts['ports'])); + } + $result = $this->spawnFromImage($imageOpts['spawn'], $imageOpts['name'], $ports); + $serviceId = $result['service_id'] ?? $result['id'] ?? ''; + echo "Service spawned: {$serviceId}\n"; + return; + } + + if ($imageOpts['clone']) { + $result = $this->cloneImage($imageOpts['clone'], $imageOpts['name']); + $imageId = $result['image_id'] ?? $result['id'] ?? ''; + echo "Image cloned: {$imageId}\n"; + return; + } + + $this->cliError("No action specified for image command. Use --list, --info, --delete, --publish, --spawn, --clone, etc."); + exit(2); + } + + /** + * Parse image-specific options. + * + * @param array $args Arguments to parse + * @return array Parsed options + */ + private function cliParseImageOptions(array $args): array { + $opts = [ + 'list' => false, + 'info' => null, + 'delete' => null, + 'lock' => null, + 'unlock' => null, + 'publish' => null, + 'sourceType' => null, + 'visibility' => null, + 'visibilityMode' => null, + 'spawn' => null, + 'clone' => null, + 'name' => null, + 'ports' => null, + ]; + + $i = 0; + while ($i < count($args)) { + $arg = $args[$i]; + + if ($arg === '--list' || $arg === '-l') { + $opts['list'] = true; + } elseif ($arg === '--info') { + $i++; + $opts['info'] = $args[$i] ?? null; + } elseif ($arg === '--delete') { + $i++; + $opts['delete'] = $args[$i] ?? null; + } elseif ($arg === '--lock') { + $i++; + $opts['lock'] = $args[$i] ?? null; + } elseif ($arg === '--unlock') { + $i++; + $opts['unlock'] = $args[$i] ?? null; + } elseif ($arg === '--publish') { + $i++; + $opts['publish'] = $args[$i] ?? null; + } elseif ($arg === '--source-type') { + $i++; + $opts['sourceType'] = $args[$i] ?? null; + } elseif ($arg === '--visibility') { + $i++; + $opts['visibility'] = $args[$i] ?? null; + // Check if next arg is the mode (not a flag) + if (isset($args[$i + 1]) && !str_starts_with($args[$i + 1], '-')) { + $i++; + $opts['visibilityMode'] = $args[$i]; + } + } elseif ($arg === '--spawn') { + $i++; + $opts['spawn'] = $args[$i] ?? null; + } elseif ($arg === '--clone') { + $i++; + $opts['clone'] = $args[$i] ?? null; + } elseif ($arg === '--name') { + $i++; + $opts['name'] = $args[$i] ?? null; + } elseif ($arg === '--ports') { + $i++; + $opts['ports'] = $args[$i] ?? null; + } + + $i++; + } + + return $opts; + } + + /** + * Print images list in tabular format. + * + * @param array $images Images to print + */ + private function cliPrintImageList(array $images): void { + if (empty($images)) { + echo "No images found.\n"; + return; + } + + printf("%-38s %-20s %-10s %-10s %s\n", 'ID', 'NAME', 'VISIBILITY', 'SOURCE', 'CREATED'); + + foreach ($images as $image) { + printf( + "%-38s %-20s %-10s %-10s %s\n", + substr($image['image_id'] ?? $image['id'] ?? '-', 0, 38), + substr($image['name'] ?? '-', 0, 20), + substr($image['visibility'] ?? 'private', 0, 10), + substr($image['source_type'] ?? '-', 0, 10), + $image['created_at'] ?? '-' + ); + } + } + + /** + * Print image info. + * + * @param array $image Image data + */ + private function cliPrintImageInfo(array $image): void { + echo "ID: " . ($image['image_id'] ?? $image['id'] ?? '-') . "\n"; + if (!empty($image['name'])) { + echo "Name: " . $image['name'] . "\n"; + } + if (!empty($image['visibility'])) { + echo "Visibility: " . $image['visibility'] . "\n"; + } + if (!empty($image['source_type'])) { + echo "Source Type: " . $image['source_type'] . "\n"; + } + if (!empty($image['source_id'])) { + echo "Source ID: " . $image['source_id'] . "\n"; + } + if (!empty($image['size'])) { + echo "Size: " . $image['size'] . "\n"; + } + if (isset($image['locked'])) { + echo "Locked: " . ($image['locked'] ? 'yes' : 'no') . "\n"; + } + if (!empty($image['created_at'])) { + echo "Created: " . $image['created_at'] . "\n"; + } + } + /** * Handle key subcommand. * @@ -2594,6 +2820,29 @@ class Unsandbox { } } + /** + * Handle languages command. + * + * @param array $args Command arguments + * @param array $opts Global options + */ + private function cliHandleLanguages(array $args, array $opts): void { + // Check for --json flag + $jsonOutput = in_array('--json', $args); + + $languages = $this->getLanguages(); + + if ($jsonOutput) { + // Output as JSON array + echo json_encode($languages) . "\n"; + } else { + // Output one language per line (pipe-friendly) + foreach ($languages as $lang) { + echo $lang . "\n"; + } + } + } + /** * Print list of resources in tabular format. * @@ -2718,6 +2967,7 @@ USAGE: php un.php service [options] php un.php snapshot [options] php un.php key + php un.php languages [--json] GLOBAL OPTIONS: -s, --shell LANG Language for inline code execution @@ -2739,6 +2989,7 @@ COMMANDS: service Manage persistent services snapshot Manage snapshots key Validate API key + languages [--json] List available languages SESSION OPTIONS: --list, -l List active sessions diff --git a/clients/powershell/sync/src/un.ps1 b/clients/powershell/sync/src/un.ps1 index 6958c34..4835111 100644 --- a/clients/powershell/sync/src/un.ps1 +++ b/clients/powershell/sync/src/un.ps1 @@ -398,6 +398,25 @@ function Invoke-Session { $result | ConvertTo-Json -Depth 5 } +function Invoke-Languages { + param($Args) + + $jsonOutput = $Args -contains "--json" + + $result = Invoke-Api -Endpoint "/languages" + $languages = $result.languages + + if ($jsonOutput) { + # Output as JSON array + $languages | ConvertTo-Json -Compress + } else { + # Output one language per line + foreach ($lang in $languages) { + Write-Output $lang + } + } +} + function Invoke-Key { param($Args) @@ -455,6 +474,126 @@ function Invoke-Key { } } +function Invoke-Image { + param($Args) + + # Parse arguments + $listMode = $Args -contains "--list" -or $Args -contains "-l" + $infoId = $null + $deleteId = $null + $lockId = $null + $unlockId = $null + $publishId = $null + $sourceType = $null + $visibilityId = $null + $visibilityMode = $null + $spawnId = $null + $cloneId = $null + $name = $null + $ports = $null + + for ($i = 0; $i -lt $Args.Count; $i++) { + switch ($Args[$i]) { + "--info" { $infoId = $Args[$i + 1]; $i++ } + "--delete" { $deleteId = $Args[$i + 1]; $i++ } + "--lock" { $lockId = $Args[$i + 1]; $i++ } + "--unlock" { $unlockId = $Args[$i + 1]; $i++ } + "--publish" { $publishId = $Args[$i + 1]; $i++ } + "--source-type" { $sourceType = $Args[$i + 1]; $i++ } + "--visibility" { + $visibilityId = $Args[$i + 1] + $visibilityMode = $Args[$i + 2] + $i += 2 + } + "--spawn" { $spawnId = $Args[$i + 1]; $i++ } + "--clone" { $cloneId = $Args[$i + 1]; $i++ } + "--name" { $name = $Args[$i + 1]; $i++ } + "--ports" { $ports = $Args[$i + 1]; $i++ } + } + } + + if ($listMode) { + $result = Invoke-Api -Endpoint "/images" + $result | ConvertTo-Json -Depth 5 + return + } + + if ($infoId) { + $result = Invoke-Api -Endpoint "/images/$infoId" + $result | ConvertTo-Json -Depth 5 + return + } + + if ($deleteId) { + Invoke-Api -Endpoint "/images/$deleteId" -Method "DELETE" + Write-Host "`e[32mImage deleted: $deleteId`e[0m" + return + } + + if ($lockId) { + Invoke-Api -Endpoint "/images/$lockId/lock" -Method "POST" -Body "{}" + Write-Host "`e[32mImage locked: $lockId`e[0m" + return + } + + if ($unlockId) { + Invoke-Api -Endpoint "/images/$unlockId/unlock" -Method "POST" -Body "{}" + Write-Host "`e[32mImage unlocked: $unlockId`e[0m" + return + } + + if ($publishId) { + if (-not $sourceType) { + Write-Error "Error: --publish requires --source-type (service or snapshot)" + exit 1 + } + $payload = @{ + source_type = $sourceType + source_id = $publishId + } + if ($name) { $payload["name"] = $name } + $body = $payload | ConvertTo-Json + $result = Invoke-Api -Endpoint "/images/publish" -Method "POST" -Body $body + Write-Host "`e[32mImage published`e[0m" + $result | ConvertTo-Json -Depth 5 + return + } + + if ($visibilityId -and $visibilityMode) { + $payload = @{ visibility = $visibilityMode } | ConvertTo-Json + Invoke-Api -Endpoint "/images/$visibilityId/visibility" -Method "POST" -Body $payload + Write-Host "`e[32mImage visibility set to $visibilityMode`: $visibilityId`e[0m" + return + } + + if ($spawnId) { + $payload = @{} + if ($name) { $payload["name"] = $name } + if ($ports) { + $portList = $ports -split "," | ForEach-Object { [int]$_ } + $payload["ports"] = $portList + } + $body = $payload | ConvertTo-Json + $result = Invoke-Api -Endpoint "/images/$spawnId/spawn" -Method "POST" -Body $body + Write-Host "`e[32mService spawned from image`e[0m" + $result | ConvertTo-Json -Depth 5 + return + } + + if ($cloneId) { + $payload = @{} + if ($name) { $payload["name"] = $name } + $body = $payload | ConvertTo-Json + $result = Invoke-Api -Endpoint "/images/$cloneId/clone" -Method "POST" -Body $body + Write-Host "`e[32mImage cloned`e[0m" + $result | ConvertTo-Json -Depth 5 + return + } + + Write-Error "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone" + exit 1 +} + function Invoke-Service { param($Args) @@ -685,8 +824,27 @@ if ($args.Count -eq 0 -or $args[0] -eq "--help" -or $args[0] -eq "-h") { Usage: pwsh un.ps1 [options] pwsh un.ps1 session [options] pwsh un.ps1 service [options] + pwsh un.ps1 image [options] + pwsh un.ps1 languages [--json] pwsh un.ps1 key [options] +Languages options: + --json Output as JSON array + +Image options: + --list, -l List all images + --info ID Get image details + --delete ID Delete an image + --lock ID Lock image to prevent deletion + --unlock ID Unlock image + --publish ID Publish image from service/snapshot + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility: private, unlisted, or public + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + Execute options: -e KEY=VALUE Environment variable -n MODE Network mode (zerotrust|semitrusted) @@ -731,6 +889,10 @@ if ($args[0] -eq "session") { Invoke-Session -Args $args[1..($args.Count-1)] } elseif ($args[0] -eq "service") { Invoke-Service -Args $args[1..($args.Count-1)] +} elseif ($args[0] -eq "image") { + Invoke-Image -Args $args[1..($args.Count-1)] +} elseif ($args[0] -eq "languages") { + Invoke-Languages -Args $args[1..($args.Count-1)] } elseif ($args[0] -eq "key") { Invoke-Key -Args $args[1..($args.Count-1)] } else { diff --git a/clients/prolog/sync/src/un.pro b/clients/prolog/sync/src/un.pro index 78902dd..2bd863c 100644 --- a/clients/prolog/sync/src/un.pro +++ b/clients/prolog/sync/src/un.pro @@ -342,10 +342,168 @@ validate_key(Extend) :- ), shell(Cmd, 0). +% Languages command +languages_command(JsonOutput) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + ( JsonOutput = true + -> % Output as JSON array + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/languages:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/languages -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -c ".languages // []"', + [SecretKey, PublicKey]) + ; % Output one language per line + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/languages:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/languages -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r ".languages[]"', + [SecretKey, PublicKey]) + ), + shell(Cmd, 0). + +% Handle languages subcommand +handle_languages(['--json'|_]) :- languages_command(true). +handle_languages(_) :- languages_command(false). + % Handle key subcommand handle_key(['--extend'|_]) :- validate_key(true). handle_key(_) :- validate_key(false). +% Image list +image_list :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/images:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/images -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq .', + [SecretKey, PublicKey]), + shell(Cmd, 0). + +% Image info +image_info(ImageId) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/images/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/images/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq .', + [ImageId, SecretKey, ImageId, PublicKey]), + shell(Cmd, 0). + +% Image delete +image_delete(ImageId) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:DELETE:/images/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X DELETE https://api.unsandbox.com/images/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mImage deleted: ~w\\x1b[0m"', + [ImageId, SecretKey, ImageId, PublicKey, ImageId]), + shell(Cmd, 0). + +% Image lock +image_lock(ImageId) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/images/~w/lock:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/images/~w/lock -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mImage locked: ~w\\x1b[0m"', + [ImageId, SecretKey, ImageId, PublicKey, ImageId]), + shell(Cmd, 0). + +% Image unlock +image_unlock(ImageId) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + format(atom(Cmd), + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/images/~w/unlock:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/images/~w/unlock -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mImage unlocked: ~w\\x1b[0m"', + [ImageId, SecretKey, ImageId, PublicKey, ImageId]), + shell(Cmd, 0). + +% Image publish +image_publish(SourceId, SourceType, Name) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + ( Name \= '' + -> format(atom(NameJson), ',\\\"name\\\":\\\"~w\\\"', [Name]) + ; NameJson = '' + ), + format(atom(Cmd), + 'BODY="{\\\"source_type\\\":\\\"~w\\\",\\\"source_id\\\":\\\"~w\\\"~w}"; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/images/publish:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/images/publish -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" | jq . && echo -e "\\x1b[32mImage published\\x1b[0m"', + [SourceType, SourceId, NameJson, SecretKey, PublicKey]), + shell(Cmd, 0). + +% Image visibility +image_visibility(ImageId, Mode) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + format(atom(Cmd), + 'BODY="{\\\"visibility\\\":\\\"~w\\\"}"; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/images/~w/visibility:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/images/~w/visibility -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" >/dev/null && echo -e "\\x1b[32mImage visibility set to ~w: ~w\\x1b[0m"', + [Mode, ImageId, SecretKey, ImageId, PublicKey, Mode, ImageId]), + shell(Cmd, 0). + +% Image spawn +image_spawn(ImageId, Name, Ports) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + ( Name \= '' + -> format(atom(NameJson), '\\\"name\\\":\\\"~w\\\"', [Name]) + ; NameJson = '' + ), + ( Ports \= '' + -> ( Name \= '' + -> format(atom(PortsJson), ',\\\"ports\\\":[~w]', [Ports]) + ; format(atom(PortsJson), '\\\"ports\\\":[~w]', [Ports]) + ) + ; PortsJson = '' + ), + format(atom(Cmd), + 'BODY="{~w~w}"; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/images/~w/spawn:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/images/~w/spawn -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" | jq . && echo -e "\\x1b[32mService spawned from image\\x1b[0m"', + [NameJson, PortsJson, ImageId, SecretKey, ImageId, PublicKey]), + shell(Cmd, 0). + +% Image clone +image_clone(ImageId, Name) :- + get_public_key(PublicKey), + get_secret_key(SecretKey), + ( Name \= '' + -> format(atom(NameJson), '\\\"name\\\":\\\"~w\\\"', [Name]) + ; NameJson = '' + ), + format(atom(Cmd), + 'BODY="{~w}"; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/images/~w/clone:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/images/~w/clone -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" | jq . && echo -e "\\x1b[32mImage cloned\\x1b[0m"', + [NameJson, ImageId, SecretKey, ImageId, PublicKey]), + shell(Cmd, 0). + +% Handle image subcommand +handle_image(['--list'|_]) :- !, image_list. +handle_image(['-l'|_]) :- !, image_list. +handle_image(['--info', ImageId|_]) :- !, image_info(ImageId). +handle_image(['--delete', ImageId|_]) :- !, image_delete(ImageId). +handle_image(['--lock', ImageId|_]) :- !, image_lock(ImageId). +handle_image(['--unlock', ImageId|_]) :- !, image_unlock(ImageId). +handle_image(['--publish', SourceId, '--source-type', SourceType|Rest]) :- !, + parse_image_name(Rest, '', Name), + image_publish(SourceId, SourceType, Name). +handle_image(['--publish', _|_]) :- !, + write(user_error, '\x1b[31mError: --publish requires --source-type (service or snapshot)\x1b[0m\n'), + halt(1). +handle_image(['--visibility', ImageId, Mode|_]) :- !, image_visibility(ImageId, Mode). +handle_image(['--spawn', ImageId|Rest]) :- !, + parse_spawn_args(Rest, '', '', Name, Ports), + image_spawn(ImageId, Name, Ports). +handle_image(['--clone', ImageId|Rest]) :- !, + parse_image_name(Rest, '', Name), + image_clone(ImageId, Name). +handle_image(_) :- + write(user_error, '\x1b[31mError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone\x1b[0m\n'), + halt(1). + +% Parse --name from arguments +parse_image_name([], Name, Name). +parse_image_name(['--name', N|_], _, N) :- !. +parse_image_name([_|Rest], Name, NameOut) :- parse_image_name(Rest, Name, NameOut). + +% Parse --name and --ports for spawn +parse_spawn_args([], Name, Ports, Name, Ports). +parse_spawn_args(['--name', N|Rest], _, Ports, NameOut, PortsOut) :- !, + parse_spawn_args(Rest, N, Ports, NameOut, PortsOut). +parse_spawn_args(['--ports', P|Rest], Name, _, NameOut, PortsOut) :- !, + parse_spawn_args(Rest, Name, P, NameOut, PortsOut). +parse_spawn_args([_|Rest], Name, Ports, NameOut, PortsOut) :- + parse_spawn_args(Rest, Name, Ports, NameOut, PortsOut). + % Handle session subcommand handle_session(['--list'|_]) :- session_list. handle_session(['-l'|_]) :- session_list. @@ -520,6 +678,22 @@ main(Argv) :- -> write(user_error, 'Usage: un.pro [options] \n'), write(user_error, ' un.pro session [options]\n'), write(user_error, ' un.pro service [options]\n'), + write(user_error, ' un.pro image [options]\n'), + write(user_error, ' un.pro languages [--json]\n'), + write(user_error, ' un.pro key [options]\n'), + write(user_error, '\n'), + write(user_error, 'Image options:\n'), + write(user_error, ' --list, -l List all images\n'), + write(user_error, ' --info ID Get image details\n'), + write(user_error, ' --delete ID Delete an image\n'), + write(user_error, ' --lock ID Lock image\n'), + write(user_error, ' --unlock ID Unlock image\n'), + write(user_error, ' --publish ID --source-type TYPE Publish image\n'), + write(user_error, ' --visibility ID MODE Set visibility\n'), + write(user_error, ' --spawn ID Spawn service from image\n'), + write(user_error, ' --clone ID Clone an image\n'), + write(user_error, ' --name NAME Name for spawned/cloned\n'), + write(user_error, ' --ports PORTS Ports for spawned service\n'), halt(1) ; true ), @@ -529,6 +703,10 @@ main(Argv) :- -> handle_session(Rest) ; Argv = ['service'|Rest] -> handle_service(Rest) + ; Argv = ['image'|Rest] + -> handle_image(Rest) + ; Argv = ['languages'|Rest] + -> handle_languages(Rest) ; Argv = ['key'|Rest] -> handle_key(Rest) ; Argv = [Filename|_] diff --git a/clients/python/sync/src/un.py b/clients/python/sync/src/un.py index e7d5902..95b3bce 100644 --- a/clients/python/sync/src/un.py +++ b/clients/python/sync/src/un.py @@ -2143,6 +2143,17 @@ def _format_list_output(items: List[Dict[str, Any]], resource_type: str) -> str: item.get("size", ""), item.get("created_at", "")[:19] if item.get("created_at") else "", ]) + elif resource_type == "image": + headers = ["ID", "NAME", "VISIBILITY", "SOURCE", "CREATED"] + rows = [] + for item in items: + rows.append([ + item.get("id", item.get("image_id", ""))[:36], + item.get("name", "")[:20], + item.get("visibility", "private"), + item.get("source_type", "")[:10], + item.get("created_at", "")[:19] if item.get("created_at") else "", + ]) else: headers = ["ID", "STATUS"] rows = [[str(item.get("id", "")), str(item.get("status", ""))] for item in items] @@ -2178,6 +2189,8 @@ Examples: python un.py service --list List all services python un.py snapshot --list List all snapshots python un.py key Check API key + python un.py languages List available languages + python un.py languages --json List languages as JSON """, ) @@ -2322,9 +2335,43 @@ Examples: snapshot_parser.add_argument("--ports", metavar="PORTS", help="Ports for cloned service") + # Image subcommand + image_parser = subparsers.add_parser("image", help="Manage images") + image_group = image_parser.add_mutually_exclusive_group() + image_group.add_argument("-l", "--list", action="store_true", + help="List all images") + image_group.add_argument("--info", metavar="ID", + help="Get image details") + image_group.add_argument("--delete", metavar="ID", + help="Delete image") + image_group.add_argument("--lock", metavar="ID", + help="Prevent deletion") + image_group.add_argument("--unlock", metavar="ID", + help="Allow deletion") + image_group.add_argument("--publish", metavar="ID", + help="Publish image from service/snapshot (requires --source-type)") + image_group.add_argument("--visibility", nargs=2, metavar=("ID", "MODE"), + help="Set visibility (private, unlisted, public)") + image_group.add_argument("--spawn", metavar="ID", + help="Spawn new service from image") + image_group.add_argument("--clone", metavar="ID", + help="Clone an image") + image_parser.add_argument("--source-type", metavar="TYPE", + choices=["service", "snapshot"], + help="Source type for publish (service or snapshot)") + image_parser.add_argument("--name", metavar="NAME", + help="Name for spawned service or cloned image") + image_parser.add_argument("--ports", metavar="PORTS", + help="Ports for spawned service (comma-separated)") + # Key subcommand subparsers.add_parser("key", help="Check API key validity") + # Languages subcommand + languages_parser = subparsers.add_parser("languages", help="List available languages") + languages_parser.add_argument("--json", action="store_true", + help="Output as JSON array") + # Positional argument for source file or inline code parser.add_argument("source", nargs="?", help="Source file or inline code (with -s)") @@ -2356,8 +2403,12 @@ def cli_main(): _handle_service_env_command(args, public_key, secret_key) elif args.command == "snapshot": _handle_snapshot_command(args, public_key, secret_key) + elif args.command == "image": + _handle_image_command(args, public_key, secret_key) elif args.command == "key": _handle_key_command(public_key, secret_key) + elif args.command == "languages": + _handle_languages_command(args, public_key, secret_key) elif args.source or args.shell: _handle_execute_command(args, public_key, secret_key) else: @@ -2680,6 +2731,73 @@ def _handle_snapshot_command(args, public_key: str, secret_key: str): sys.exit(2) +def _handle_image_command(args, public_key: str, secret_key: str): + """Handle image subcommand.""" + if args.list: + images = list_images(public_key=public_key, secret_key=secret_key) + print(_format_list_output(images, "image")) + elif args.info: + image = get_image(args.info, public_key=public_key, secret_key=secret_key) + print(json.dumps(image, indent=2)) + elif args.delete: + result = delete_image(args.delete, public_key=public_key, secret_key=secret_key) + print(f"Image {args.delete} deleted") + elif args.lock: + result = lock_image(args.lock, public_key=public_key, secret_key=secret_key) + print(f"Image {args.lock} locked") + elif args.unlock: + result = unlock_image(args.unlock, public_key=public_key, secret_key=secret_key) + print(f"Image {args.unlock} unlocked") + elif args.publish: + if not args.source_type: + print("Error: --source-type required for --publish", file=sys.stderr) + sys.exit(2) + result = image_publish( + source_type=args.source_type, + source_id=args.publish, + name=args.name, + public_key=public_key, + secret_key=secret_key, + ) + image_id = result.get("image_id", result.get("id", "")) + print(f"Image published: {image_id}") + elif args.visibility: + image_id, mode = args.visibility + if mode not in ("private", "unlisted", "public"): + print(f"Error: Invalid visibility mode '{mode}'. Must be private, unlisted, or public", file=sys.stderr) + sys.exit(2) + result = set_image_visibility(image_id, mode, public_key=public_key, secret_key=secret_key) + print(f"Image {image_id} visibility set to {mode}") + elif args.spawn: + if not args.name: + print("Error: --name required for --spawn", file=sys.stderr) + sys.exit(2) + ports = None + if args.ports: + ports = [int(p.strip()) for p in args.ports.split(",")] + result = spawn_from_image( + args.spawn, + name=args.name, + ports=ports, + public_key=public_key, + secret_key=secret_key, + ) + service_id = result.get("service_id", result.get("id", "")) + print(f"Service spawned: {service_id}") + elif args.clone: + result = clone_image( + args.clone, + name=args.name, + public_key=public_key, + secret_key=secret_key, + ) + image_id = result.get("image_id", result.get("id", "")) + print(f"Image cloned: {image_id}") + else: + print("Error: No action specified for image command", file=sys.stderr) + sys.exit(2) + + def _handle_key_command(public_key: str, secret_key: str): """Handle key validation command.""" result = validate_keys(public_key, secret_key) @@ -2694,5 +2812,18 @@ def _handle_key_command(public_key: str, secret_key: str): print(f"Reason: {result.get('reason')}") +def _handle_languages_command(args, public_key: str, secret_key: str): + """Handle languages list command.""" + languages = get_languages(public_key, secret_key) + + if args.json: + # Output as JSON array + print(json.dumps(languages)) + else: + # Output one language per line (pipe-friendly) + for lang in languages: + print(lang) + + if __name__ == "__main__": cli_main() diff --git a/clients/r/sync/src/un.r b/clients/r/sync/src/un.r index 5445a72..5863c7b 100644 --- a/clients/r/sync/src/un.r +++ b/clients/r/sync/src/un.r @@ -1002,6 +1002,25 @@ cmd_session <- function(args) { cat(sprintf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n", YELLOW, RESET)) } +cmd_languages <- function(args) { + keys <- get_api_keys(args$api_key) + public_key <- keys$public_key + secret_key <- keys$secret_key + + result <- api_request("/languages", public_key, secret_key) + langs <- result$languages + + if (!is.null(args$json_output) && args$json_output) { + # JSON output - print as JSON array + cat(toJSON(langs, auto_unbox = TRUE), "\n") + } else { + # Default output - one language per line + for (lang in langs) { + cat(lang, "\n") + } + } +} + cmd_key <- function(args) { keys <- get_api_keys(args$api_key) public_key <- keys$public_key @@ -1181,6 +1200,113 @@ cmd_snapshot <- function(args) { quit(status = 1) } +cmd_image <- function(args) { + keys <- get_api_keys(args$api_key) + public_key <- keys$public_key + secret_key <- keys$secret_key + + if (!is.null(args$list) && args$list) { + result <- api_request("/images", public_key, secret_key) + images <- if (!is.null(result$images)) result$images else list() + if (length(images) == 0) { + cat("No images found\n") + } else { + cat(sprintf("%-40s %-20s %-12s %s\n", "ID", "Name", "Visibility", "Created")) + for (img in images) { + cat(sprintf("%-40s %-20s %-12s %s\n", + if (!is.null(img$id)) img$id else "N/A", + if (!is.null(img$name)) img$name else "-", + if (!is.null(img$visibility)) img$visibility else "N/A", + if (!is.null(img$created_at)) img$created_at else "N/A")) + } + } + return() + } + + if (!is.null(args$image_info)) { + result <- api_request(paste0("/images/", args$image_info), public_key, secret_key) + cat(sprintf("%sImage Details%s\n\n", BLUE, RESET)) + cat(sprintf("Image ID: %s\n", if (!is.null(result$id)) result$id else "N/A")) + cat(sprintf("Name: %s\n", if (!is.null(result$name)) result$name else "-")) + cat(sprintf("Visibility: %s\n", if (!is.null(result$visibility)) result$visibility else "N/A")) + cat(sprintf("Created: %s\n", if (!is.null(result$created_at)) result$created_at else "N/A")) + return() + } + + if (!is.null(args$image_delete)) { + result <- api_request(paste0("/images/", args$image_delete), public_key, secret_key, method = "DELETE") + cat(sprintf("%sImage deleted successfully%s\n", GREEN, RESET)) + return() + } + + if (!is.null(args$image_lock)) { + result <- api_request(paste0("/images/", args$image_lock, "/lock"), public_key, secret_key, method = "POST", data = list()) + cat(sprintf("%sImage locked successfully%s\n", GREEN, RESET)) + return() + } + + if (!is.null(args$image_unlock)) { + result <- api_request(paste0("/images/", args$image_unlock, "/unlock"), public_key, secret_key, method = "POST", data = list()) + cat(sprintf("%sImage unlocked successfully%s\n", GREEN, RESET)) + return() + } + + if (!is.null(args$image_publish)) { + if (is.null(args$source_type)) { + cat(sprintf("%sError: --source-type required for --publish (service or snapshot)%s\n", RED, RESET), file = stderr()) + quit(status = 1) + } + payload <- list(source_type = args$source_type, source_id = args$image_publish) + if (!is.null(args$name)) { + payload$name <- args$name + } + result <- api_request("/images/publish", public_key, secret_key, method = "POST", data = payload) + cat(sprintf("%sImage published successfully%s\n", GREEN, RESET)) + cat(sprintf("Image ID: %s\n", if (!is.null(result$id)) result$id else "N/A")) + return() + } + + if (!is.null(args$image_visibility)) { + if (is.null(args$visibility_mode)) { + cat(sprintf("%sError: visibility mode required (private, unlisted, or public)%s\n", RED, RESET), file = stderr()) + quit(status = 1) + } + payload <- list(visibility = args$visibility_mode) + result <- api_request(paste0("/images/", args$image_visibility, "/visibility"), public_key, secret_key, method = "POST", data = payload) + cat(sprintf("%sImage visibility set to %s%s\n", GREEN, args$visibility_mode, RESET)) + return() + } + + if (!is.null(args$image_spawn)) { + payload <- list() + if (!is.null(args$name)) { + payload$name <- args$name + } + if (!is.null(args$ports)) { + ports_vec <- as.integer(strsplit(args$ports, ",")[[1]]) + payload$ports <- ports_vec + } + result <- api_request(paste0("/images/", args$image_spawn, "/spawn"), public_key, secret_key, method = "POST", data = payload) + cat(sprintf("%sService spawned from image%s\n", GREEN, RESET)) + cat(sprintf("Service ID: %s\n", if (!is.null(result$id)) result$id else "N/A")) + return() + } + + if (!is.null(args$image_clone)) { + payload <- list() + if (!is.null(args$name)) { + payload$name <- args$name + } + result <- api_request(paste0("/images/", args$image_clone, "/clone"), public_key, secret_key, method = "POST", data = payload) + cat(sprintf("%sImage cloned successfully%s\n", GREEN, RESET)) + cat(sprintf("Image ID: %s\n", if (!is.null(result$id)) result$id else "N/A")) + return() + } + + cat(sprintf("%sError: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID%s\n", RED, RESET), file = stderr()) + quit(status = 1) +} + cmd_service <- function(args) { # Handle env subcommand if (!is.null(args$env_action) && args$env_action != "") { @@ -1433,7 +1559,18 @@ parse_args <- function() { svc_envs = NULL, svc_env_file = NULL, env_action = NULL, - env_target = NULL + env_target = NULL, + json_output = FALSE, + image_info = NULL, + image_delete = NULL, + image_lock = NULL, + image_unlock = NULL, + image_publish = NULL, + source_type = NULL, + image_visibility = NULL, + visibility_mode = NULL, + image_spawn = NULL, + image_clone = NULL ) i <- 1 @@ -1461,9 +1598,18 @@ parse_args <- function() { } else if (arg == "key") { result$command <- "key" i <- i + 1 + } else if (arg == "languages") { + result$command <- "languages" + i <- i + 1 + } else if (arg == "--json") { + result$json_output <- TRUE + i <- i + 1 } else if (arg == "snapshot") { result$command <- "snapshot" i <- i + 1 + } else if (arg == "image") { + result$command <- "image" + i <- i + 1 } else if (arg %in% c("-k", "--api-key")) { i <- i + 1 result$api_key <- args[i] @@ -1504,7 +1650,11 @@ parse_args <- function() { i <- i + 1 } else if (arg == "--info") { i <- i + 1 - result$info <- args[i] + if (!is.null(result$command) && result$command == "image") { + result$image_info <- args[i] + } else { + result$info <- args[i] + } i <- i + 1 } else if (arg == "--logs") { i <- i + 1 @@ -1591,11 +1741,27 @@ parse_args <- function() { i <- i + 1 } else if (arg == "--delete") { i <- i + 1 - result$delete <- args[i] + if (!is.null(result$command) && result$command == "image") { + result$image_delete <- args[i] + } else { + result$delete <- args[i] + } + i <- i + 1 + } else if (arg == "--lock") { + i <- i + 1 + result$image_lock <- args[i] + i <- i + 1 + } else if (arg == "--unlock") { + i <- i + 1 + result$image_unlock <- args[i] i <- i + 1 } else if (arg == "--clone") { i <- i + 1 - result$clone <- args[i] + if (!is.null(result$command) && result$command == "image") { + result$image_clone <- args[i] + } else { + result$clone <- args[i] + } i <- i + 1 } else if (arg == "--shell") { i <- i + 1 @@ -1604,6 +1770,26 @@ parse_args <- function() { } else if (arg == "--extend") { result$extend <- TRUE i <- i + 1 + } else if (arg == "--source-type") { + i <- i + 1 + result$source_type <- args[i] + i <- i + 1 + } else if (arg == "--visibility") { + i <- i + 1 + result$image_visibility <- args[i] + i <- i + 1 + if (i <= length(args) && !startsWith(args[i], "-")) { + result$visibility_mode <- args[i] + i <- i + 1 + } + } else if (arg == "--publish") { + i <- i + 1 + result$image_publish <- args[i] + i <- i + 1 + } else if (arg == "--spawn") { + i <- i + 1 + result$image_spawn <- args[i] + i <- i + 1 } else if (!startsWith(arg, "-")) { result$source_file <- arg i <- i + 1 @@ -1636,8 +1822,12 @@ main <- function() { cmd_service(args) } else if (!is.null(args$command) && args$command == "snapshot") { cmd_snapshot(args) + } else if (!is.null(args$command) && args$command == "image") { + cmd_image(args) } else if (!is.null(args$command) && args$command == "key") { cmd_key(args) + } else if (!is.null(args$command) && args$command == "languages") { + cmd_languages(args) } else if (!is.null(args$source_file)) { cmd_execute(args) } else { @@ -1646,12 +1836,29 @@ main <- function() { cat(" un.r service [options]\n", file = stderr()) cat(" un.r service env [options]\n", file = stderr()) cat(" un.r snapshot [options]\n", file = stderr()) + cat(" un.r image [options]\n", file = stderr()) cat(" un.r key [options]\n", file = stderr()) + cat(" un.r languages [--json]\n", file = stderr()) + cat("\nLanguages options:\n", file = stderr()) + cat(" --json Output as JSON array\n", file = stderr()) cat("\nService env commands:\n", file = stderr()) cat(" env status Show vault status\n", file = stderr()) cat(" env set Set vault (-e KEY=VALUE or --env-file FILE)\n", file = stderr()) cat(" env export Export vault contents\n", file = stderr()) cat(" env delete Delete vault\n", file = stderr()) + cat("\nImage commands:\n", file = stderr()) + cat(" --list List all images\n", file = stderr()) + cat(" --info ID Get image details\n", file = stderr()) + cat(" --delete ID Delete an image\n", file = stderr()) + cat(" --lock ID Lock image to prevent deletion\n", file = stderr()) + cat(" --unlock ID Unlock image\n", file = stderr()) + cat(" --publish ID Publish image from service/snapshot\n", file = stderr()) + cat(" --source-type TYPE Source type: service or snapshot\n", file = stderr()) + cat(" --visibility ID MODE Set visibility: private, unlisted, public\n", file = stderr()) + cat(" --spawn ID Spawn new service from image\n", file = stderr()) + cat(" --clone ID Clone an image\n", file = stderr()) + cat(" --name NAME Name for spawned service or cloned image\n", file = stderr()) + cat(" --ports PORTS Ports for spawned service\n", file = stderr()) quit(status = 1) } } diff --git a/clients/raku/sync/src/un.raku b/clients/raku/sync/src/un.raku index 15bec81..83bf909 100644 --- a/clients/raku/sync/src/un.raku +++ b/clients/raku/sync/src/un.raku @@ -1115,6 +1115,30 @@ sub cmd-service(@args) { exit 1; } +sub cmd-languages(@args) { + my ($public-key, $secret-key) = get-credentials(); + my $json-output = False; + + for @args -> $arg { + if $arg eq '--json' { + $json-output = True; + } + } + + my %result = languages(:$public-key, :$secret-key); + my @langs = %result.list; + + if $json-output { + # JSON array output + say to-json(@langs); + } else { + # One language per line (default) + for @langs -> $lang { + say $lang; + } + } +} + sub cmd-key(@args) { my ($public-key, $secret-key) = get-credentials(); my $extend = False; @@ -1175,12 +1199,200 @@ sub cmd-key(@args) { say "Concurrency: {%result // 'N/A'}"; } +sub cmd-image(@args) { + my ($public-key, $secret-key) = get-credentials(); + my $list-mode = False; + my $info-id = ''; + my $delete-id = ''; + my $lock-id = ''; + my $unlock-id = ''; + my $publish-id = ''; + my $source-type = ''; + my $visibility-id = ''; + my $visibility-mode = ''; + my $spawn-id = ''; + my $clone-id = ''; + my $name = ''; + my $ports = ''; + + # Parse arguments + my $i = 0; + while $i < @args.elems { + given @args[$i] { + when '--list' { + $list-mode = True; + } + when '-l' { + $list-mode = True; + } + when '--info' { + $i++; + $info-id = @args[$i]; + } + when '--delete' { + $i++; + $delete-id = @args[$i]; + } + when '--lock' { + $i++; + $lock-id = @args[$i]; + } + when '--unlock' { + $i++; + $unlock-id = @args[$i]; + } + when '--publish' { + $i++; + $publish-id = @args[$i]; + } + when '--source-type' { + $i++; + $source-type = @args[$i]; + } + when '--visibility' { + $i++; + $visibility-id = @args[$i]; + $i++; + $visibility-mode = @args[$i] if $i < @args.elems && !@args[$i].starts-with('-'); + } + when '--spawn' { + $i++; + $spawn-id = @args[$i]; + } + when '--clone' { + $i++; + $clone-id = @args[$i]; + } + when '--name' { + $i++; + $name = @args[$i]; + } + when '--ports' { + $i++; + $ports = @args[$i]; + } + } + $i++; + } + + if $list-mode { + my %result = api-request('/images', 'GET', :$public-key, :$secret-key); + my @images = %result.list; + unless @images { + say "No images found"; + return; + } + say sprintf("%-40s %-20s %-12s %s", 'ID', 'Name', 'Visibility', 'Created'); + for @images -> %img { + say sprintf("%-40s %-20s %-12s %s", + %img // 'N/A', %img // '-', %img // 'N/A', %img // 'N/A'); + } + return; + } + + if $info-id { + my %result = api-request("/images/$info-id", 'GET', :$public-key, :$secret-key); + say "{$BLUE}Image Details{$RESET}"; + say ""; + say "Image ID: {%result // 'N/A'}"; + say "Name: {%result // '-'}"; + say "Visibility: {%result // 'N/A'}"; + say "Created: {%result // 'N/A'}"; + return; + } + + if $delete-id { + api-request("/images/$delete-id", 'DELETE', :$public-key, :$secret-key); + say "{$GREEN}Image deleted successfully{$RESET}"; + return; + } + + if $lock-id { + api-request("/images/$lock-id/lock", 'POST', :$public-key, :$secret-key); + say "{$GREEN}Image locked successfully{$RESET}"; + return; + } + + if $unlock-id { + api-request("/images/$unlock-id/unlock", 'POST', :$public-key, :$secret-key); + say "{$GREEN}Image unlocked successfully{$RESET}"; + return; + } + + if $publish-id { + unless $source-type { + note "{$RED}Error: --source-type required for --publish (service or snapshot){$RESET}"; + exit 1; + } + my %payload = source_type => $source-type, source_id => $publish-id; + %payload = $name if $name; + my %result = api-request('/images/publish', 'POST', %payload, :$public-key, :$secret-key); + say "{$GREEN}Image published successfully{$RESET}"; + say "Image ID: {%result // 'N/A'}"; + return; + } + + if $visibility-id { + unless $visibility-mode { + note "{$RED}Error: visibility mode required (private, unlisted, or public){$RESET}"; + exit 1; + } + my %payload = visibility => $visibility-mode; + api-request("/images/$visibility-id/visibility", 'POST', %payload, :$public-key, :$secret-key); + say "{$GREEN}Image visibility set to $visibility-mode{$RESET}"; + return; + } + + if $spawn-id { + my %payload; + %payload = $name if $name; + if $ports { + %payload = $ports.split(',')>>.Int; + } + my %result = api-request("/images/$spawn-id/spawn", 'POST', %payload, :$public-key, :$secret-key); + say "{$GREEN}Service spawned from image{$RESET}"; + say "Service ID: {%result // 'N/A'}"; + return; + } + + if $clone-id { + my %payload; + %payload = $name if $name; + my %result = api-request("/images/$clone-id/clone", 'POST', %payload, :$public-key, :$secret-key); + say "{$GREEN}Image cloned successfully{$RESET}"; + say "Image ID: {%result // 'N/A'}"; + return; + } + + note "{$RED}Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID{$RESET}"; + exit 1; +} + sub MAIN(*@args) is export { unless @args { note "Usage: un.raku [options] "; note " un.raku session [options]"; note " un.raku service [options]"; + note " un.raku image [options]"; note " un.raku key [options]"; + note " un.raku languages [--json]"; + note ""; + note "Languages options:"; + note " --json Output as JSON array"; + note ""; + note "Image options:"; + note " --list List all images"; + note " --info ID Get image details"; + note " --delete ID Delete an image"; + note " --lock ID Lock image to prevent deletion"; + note " --unlock ID Unlock image"; + note " --publish ID Publish image from service/snapshot"; + note " --source-type TYPE Source type: service or snapshot"; + note " --visibility ID MODE Set visibility: private, unlisted, public"; + note " --spawn ID Spawn new service from image"; + note " --clone ID Clone an image"; + note " --name NAME Name for spawned service or cloned image"; + note " --ports PORTS Ports for spawned service"; exit 1; } @@ -1191,9 +1403,15 @@ sub MAIN(*@args) is export { when 'service' { cmd-service(@args[1..*]); } + when 'image' { + cmd-image(@args[1..*]); + } when 'key' { cmd-key(@args[1..*]); } + when 'languages' { + cmd-languages(@args[1..*]); + } default { cmd-execute(@args); } diff --git a/clients/ruby/sync/src/un.rb b/clients/ruby/sync/src/un.rb index 7bef484..5ec687f 100644 --- a/clients/ruby/sync/src/un.rb +++ b/clients/ruby/sync/src/un.rb @@ -1578,9 +1578,15 @@ module Un when 'snapshot' ARGV.shift cli_snapshot(options) + when 'image' + ARGV.shift + cli_image(options) when 'key' ARGV.shift cli_key(options) + when 'languages' + ARGV.shift + cli_languages(options) when '-h', '--help', 'help' cli_show_help exit(EXIT_SUCCESS) @@ -1615,6 +1621,7 @@ module Un ruby un.rb service [options] Manage services ruby un.rb snapshot [options] Manage snapshots ruby un.rb key Check API key + ruby un.rb languages [--json] List available languages Global Options: -s, --shell LANG Language for inline code @@ -2387,6 +2394,196 @@ module Un HELP end + # Image subcommand + def cli_image(options) + image_opts = { + list: false, + info: nil, + delete: nil, + lock: nil, + unlock: nil, + publish: nil, + source_type: nil, + visibility: nil, + visibility_mode: nil, + spawn: nil, + clone: nil, + name: nil, + ports: nil + } + + parser = parse_global_options(options) do + cli_image_help + end + + parser.on('-l', '--list', 'List all images') do + image_opts[:list] = true + end + parser.on('--info ID', 'Get image details') do |v| + image_opts[:info] = v + end + parser.on('--delete ID', 'Delete image') do |v| + image_opts[:delete] = v + end + parser.on('--lock ID', 'Prevent deletion') do |v| + image_opts[:lock] = v + end + parser.on('--unlock ID', 'Allow deletion') do |v| + image_opts[:unlock] = v + end + parser.on('--publish ID', 'Publish image from service/snapshot') do |v| + image_opts[:publish] = v + end + parser.on('--source-type TYPE', 'Source type: service or snapshot') do |v| + image_opts[:source_type] = v + end + parser.on('--visibility ID MODE', 'Set visibility (private, unlisted, public)') do |v| + image_opts[:visibility] = v + end + parser.on('--spawn ID', 'Spawn new service from image') do |v| + image_opts[:spawn] = v + end + parser.on('--clone ID', 'Clone an image') do |v| + image_opts[:clone] = v + end + parser.on('--name NAME', 'Name for spawned service or cloned image') do |v| + image_opts[:name] = v + end + parser.on('--ports PORTS', 'Ports for spawned service') do |v| + image_opts[:ports] = v.split(',').map(&:to_i) + end + + parser.parse!(ARGV) + + # Get visibility mode from remaining args if --visibility was used + if image_opts[:visibility] && ARGV.length > 0 && !ARGV[0].start_with?('-') + image_opts[:visibility_mode] = ARGV.shift + end + + creds = { public_key: options[:public_key], secret_key: options[:secret_key] } + + if image_opts[:list] + images = list_images(**creds) + cli_print_images_table(images) + elsif image_opts[:info] + image = get_image(image_opts[:info], **creds) + cli_print_image_info(image) + elsif image_opts[:delete] + delete_image(image_opts[:delete], **creds) + puts "Image #{image_opts[:delete]} deleted" + elsif image_opts[:lock] + lock_image(image_opts[:lock], **creds) + puts "Image #{image_opts[:lock]} locked" + elsif image_opts[:unlock] + unlock_image(image_opts[:unlock], **creds) + puts "Image #{image_opts[:unlock]} unlocked" + elsif image_opts[:publish] + unless image_opts[:source_type] + $stderr.puts 'Error: --source-type required for --publish' + exit(EXIT_INVALID_ARGS) + end + result = image_publish( + image_opts[:source_type], + image_opts[:publish], + name: image_opts[:name], + **creds + ) + image_id = result['image_id'] || result['id'] + puts "Image published: #{image_id}" + elsif image_opts[:visibility] && image_opts[:visibility_mode] + unless %w[private unlisted public].include?(image_opts[:visibility_mode]) + $stderr.puts 'Error: visibility must be private, unlisted, or public' + exit(EXIT_INVALID_ARGS) + end + set_image_visibility(image_opts[:visibility], image_opts[:visibility_mode], **creds) + puts "Image #{image_opts[:visibility]} visibility set to #{image_opts[:visibility_mode]}" + elsif image_opts[:spawn] + unless image_opts[:name] + $stderr.puts 'Error: --name required for --spawn' + exit(EXIT_INVALID_ARGS) + end + result = spawn_from_image( + image_opts[:spawn], + name: image_opts[:name], + ports: image_opts[:ports], + **creds + ) + service_id = result['service_id'] || result['id'] + puts "Service spawned: #{service_id}" + elsif image_opts[:clone] + result = clone_image( + image_opts[:clone], + name: image_opts[:name], + **creds + ) + image_id = result['image_id'] || result['id'] + puts "Image cloned: #{image_id}" + else + cli_image_help + exit(EXIT_INVALID_ARGS) + end + end + + # Print images table + def cli_print_images_table(images) + if images.empty? + puts 'No images' + return + end + + puts format('%-38s %-20s %-10s %-10s %-20s', 'ID', 'NAME', 'VISIBILITY', 'SOURCE', 'CREATED') + images.each do |img| + puts format('%-38s %-20s %-10s %-10s %-20s', + img['image_id'] || img['id'] || '-', + (img['name'] || '-')[0..19], + img['visibility'] || 'private', + (img['source_type'] || '-')[0..9], + img['created_at'] || '-') + end + end + + # Print image info + def cli_print_image_info(image) + puts "ID: #{image['image_id'] || image['id']}" + puts "Name: #{image['name']}" if image['name'] + puts "Visibility: #{image['visibility']}" if image['visibility'] + puts "Source Type: #{image['source_type']}" if image['source_type'] + puts "Source ID: #{image['source_id']}" if image['source_id'] + puts "Size: #{image['size']}" if image['size'] + puts "Locked: #{image['locked']}" if image.key?('locked') + puts "Created: #{image['created_at']}" if image['created_at'] + end + + # Image help + def cli_image_help + puts <<~HELP + Image Management + + Usage: + ruby un.rb image [options] + + Options: + -l, --list List all images + --info ID Get image details + --delete ID Delete image + --lock ID Prevent deletion + --unlock ID Allow deletion + --publish ID Publish image (requires --source-type) + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility (private, unlisted, public) + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + + Examples: + ruby un.rb image --list + ruby un.rb image --publish svc123 --source-type service --name myimage + ruby un.rb image --spawn img123 --name myservice --ports 80,443 + ruby un.rb image --visibility img123 public + HELP + end + # Key command def cli_key(options) parser = parse_global_options(options) do @@ -2414,6 +2611,32 @@ module Un raise end end + + # Languages command - list available languages + def cli_languages(options) + json_output = false + parser = parse_global_options(options) do + puts 'Usage: ruby un.rb languages [--json]' + puts + puts 'List available programming languages' + end + parser.on('--json', 'Output as JSON array') do + json_output = true + end + parser.parse!(ARGV) + + creds = { public_key: options[:public_key], secret_key: options[:secret_key] } + languages = get_languages(**creds) + + if json_output + # Output as JSON array + require 'json' + puts JSON.generate(languages) + else + # Output one language per line (pipe-friendly) + languages.each { |lang| puts lang } + end + end end end diff --git a/clients/rust/sync/src/lib.rs b/clients/rust/sync/src/lib.rs index d224886..465ae14 100644 --- a/clients/rust/sync/src/lib.rs +++ b/clients/rust/sync/src/lib.rs @@ -2288,6 +2288,24 @@ struct CliOptions { clone_name: Option, clone_shell: Option, clone_ports: Option, + + // Image options + image_list: bool, + image_info: Option, + image_delete: Option, + image_lock: Option, + image_unlock: Option, + image_publish: Option, + image_source_type: Option, + image_visibility_id: Option, + image_visibility_mode: Option, + image_spawn: Option, + image_clone: Option, + image_name: Option, + image_ports: Option, + + // Languages options + languages_json: bool, } fn print_help() { @@ -2299,7 +2317,9 @@ USAGE: un session [OPTIONS] Interactive session un service [OPTIONS] Manage services un snapshot [OPTIONS] Manage snapshots + un image [OPTIONS] Manage images un key Check API key + un languages [--json] List supported languages GLOBAL OPTIONS: -s, --shell LANG Language for inline code execution @@ -2356,6 +2376,21 @@ SNAPSHOT COMMANDS: un snapshot --unlock ID Allow deletion un snapshot --clone ID Clone snapshot +IMAGE COMMANDS: + un image --list List all images + un image --info ID Get image details + un image --delete ID Delete an image + un image --lock ID Lock image to prevent deletion + un image --unlock ID Unlock image + un image --publish ID --source-type T Publish from service/snapshot + un image --visibility ID MODE Set visibility (private/unlisted/public) + un image --spawn ID --name NAME Spawn service from image + un image --clone ID --name NAME Clone an image + +LANGUAGES COMMAND: + un languages List supported languages (one per line) + un languages --json List as JSON array + EXAMPLES: un script.py Execute Python script un -s bash 'echo hello' Run inline bash command @@ -2463,10 +2498,11 @@ fn parse_args(args: &[String]) -> CliOptions { i += 1; } "-l" | "--list" => { - // Used by session, service, snapshot + // Used by session, service, snapshot, image opts.session_list = true; opts.service_list = true; opts.snapshot_list = true; + opts.image_list = true; i += 1; } "--attach" => { @@ -2557,6 +2593,7 @@ fn parse_args(args: &[String]) -> CliOptions { if i + 1 < args.len() { opts.service_name = Some(args[i + 1].clone()); opts.clone_name = Some(args[i + 1].clone()); + opts.image_name = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2566,6 +2603,7 @@ fn parse_args(args: &[String]) -> CliOptions { if i + 1 < args.len() { opts.service_ports = Some(args[i + 1].clone()); opts.clone_ports = Some(args[i + 1].clone()); + opts.image_ports = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2616,6 +2654,7 @@ fn parse_args(args: &[String]) -> CliOptions { if i + 1 < args.len() { opts.service_info = Some(args[i + 1].clone()); opts.snapshot_info = Some(args[i + 1].clone()); + opts.image_info = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2649,6 +2688,7 @@ fn parse_args(args: &[String]) -> CliOptions { if i + 1 < args.len() { opts.service_lock = Some(args[i + 1].clone()); opts.snapshot_lock = Some(args[i + 1].clone()); + opts.image_lock = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2658,6 +2698,7 @@ fn parse_args(args: &[String]) -> CliOptions { if i + 1 < args.len() { opts.service_unlock = Some(args[i + 1].clone()); opts.snapshot_unlock = Some(args[i + 1].clone()); + opts.image_unlock = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2695,6 +2736,7 @@ fn parse_args(args: &[String]) -> CliOptions { "--delete" => { if i + 1 < args.len() { opts.snapshot_delete = Some(args[i + 1].clone()); + opts.image_delete = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2703,6 +2745,47 @@ fn parse_args(args: &[String]) -> CliOptions { "--clone" => { if i + 1 < args.len() { opts.snapshot_clone = Some(args[i + 1].clone()); + opts.image_clone = Some(args[i + 1].clone()); + i += 2; + } else { + i += 1; + } + } + "--json" => { + opts.languages_json = true; + i += 1; + } + "--publish" => { + if i + 1 < args.len() { + opts.image_publish = Some(args[i + 1].clone()); + i += 2; + } else { + i += 1; + } + } + "--source-type" => { + if i + 1 < args.len() { + opts.image_source_type = Some(args[i + 1].clone()); + i += 2; + } else { + i += 1; + } + } + "--visibility" => { + if i + 2 < args.len() { + opts.image_visibility_id = Some(args[i + 1].clone()); + opts.image_visibility_mode = Some(args[i + 2].clone()); + i += 3; + } else if i + 1 < args.len() { + opts.image_visibility_id = Some(args[i + 1].clone()); + i += 2; + } else { + i += 1; + } + } + "--spawn" => { + if i + 1 < args.len() { + opts.image_spawn = Some(args[i + 1].clone()); i += 2; } else { i += 1; @@ -2712,7 +2795,7 @@ fn parse_args(args: &[String]) -> CliOptions { // Positional argument or subcommand if opts.command.is_none() && !arg.starts_with('-') { match arg.as_str() { - "session" | "service" | "snapshot" | "key" | "env" => { + "session" | "service" | "snapshot" | "image" | "key" | "env" | "languages" => { if opts.command.is_some() { opts.subcommand = Some(arg.clone()); } else { @@ -3486,6 +3569,201 @@ fn cmd_snapshot(opts: &CliOptions) -> i32 { } } +fn format_image_list(images: &[LxdImage]) { + if images.is_empty() { + println!("No images."); + return; + } + println!("{:<40} {:<20} {:<10} {}", "ID", "NAME", "VISIBILITY", "CREATED"); + for img in images { + println!( + "{:<40} {:<20} {:<10} {}", + img.image_id, img.name, img.visibility, img.created_at + ); + } +} + +fn cmd_image(opts: &CliOptions) -> i32 { + let creds = match get_credentials(opts) { + Ok(c) => c, + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_AUTH_ERROR; + } + }; + + // Handle --list + if opts.image_list || opts.service_list { + match list_images(None, &creds) { + Ok(images) => { + format_image_list(&images); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --info + if let Some(ref id) = opts.image_info { + match get_image(id, &creds) { + Ok(img) => { + println!("Image ID: {}", img.image_id); + println!("Name: {}", img.name); + println!("Description: {}", img.description); + println!("Source Type: {}", img.source_type); + println!("Source ID: {}", img.source_id); + println!("Visibility: {}", img.visibility); + println!("Locked: {}", img.locked); + println!("Size: {} bytes", img.size_bytes); + println!("Created: {}", img.created_at); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --delete + if let Some(ref id) = opts.image_delete { + match delete_image(id, &creds) { + Ok(()) => { + println!("Image {} deleted.", id); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --lock + if let Some(ref id) = opts.image_lock { + match lock_image(id, &creds) { + Ok(()) => { + println!("Image {} locked.", id); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --unlock + if let Some(ref id) = opts.image_unlock { + match unlock_image(id, &creds) { + Ok(()) => { + println!("Image {} unlocked.", id); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --publish + if let Some(ref source_id) = opts.image_publish { + let source_type = match &opts.image_source_type { + Some(t) => t.as_str(), + None => { + eprintln!("Error: --publish requires --source-type (service or snapshot)"); + return EXIT_INVALID_ARGS; + } + }; + let name = opts.image_name.clone().unwrap_or_else(|| "".to_string()); + match image_publish(source_type, source_id, &name, None, &creds) { + Ok(img) => { + println!("Image published: {}", img.image_id); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --visibility + if let Some(ref id) = opts.image_visibility_id { + let mode = match &opts.image_visibility_mode { + Some(m) => m.as_str(), + None => { + eprintln!("Error: --visibility requires a mode (private, unlisted, or public)"); + return EXIT_INVALID_ARGS; + } + }; + match set_image_visibility(id, mode, &creds) { + Ok(()) => { + println!("Image {} visibility set to {}.", id, mode); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --spawn + if let Some(ref id) = opts.image_spawn { + let name = opts.image_name.clone().unwrap_or_else(|| "spawned-service".to_string()); + let ports_str = opts.image_ports.clone().unwrap_or_else(|| "".to_string()); + let ports: Vec = ports_str + .split(',') + .filter_map(|p| p.trim().parse().ok()) + .collect(); + match spawn_from_image(id, &name, &ports, None, &creds) { + Ok(result) => { + println!("Service spawned: {}", result.service_id); + println!("Name: {}", result.name); + println!("URL: {}", result.url); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Handle --clone + if let Some(ref id) = opts.image_clone { + let name = opts.image_name.clone().unwrap_or_else(|| format!("{}-clone", id)); + match clone_image(id, &name, None, &creds) { + Ok(img) => { + println!("Image cloned: {}", img.image_id); + println!("Name: {}", img.name); + return EXIT_SUCCESS; + } + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_API_ERROR; + } + } + } + + // Default: list images + match list_images(None, &creds) { + Ok(images) => { + format_image_list(&images); + EXIT_SUCCESS + } + Err(e) => { + eprintln!("Error: {}", e); + EXIT_API_ERROR + } + } +} + fn cmd_key(opts: &CliOptions) -> i32 { let creds = match get_credentials(opts) { Ok(c) => c, @@ -3522,6 +3800,36 @@ fn cmd_key(opts: &CliOptions) -> i32 { } } +fn cmd_languages(opts: &CliOptions) -> i32 { + let creds = match get_credentials(opts) { + Ok(c) => c, + Err(e) => { + eprintln!("Error: {}", e); + return EXIT_AUTH_ERROR; + } + }; + + match get_languages(&creds) { + Ok(languages) => { + if opts.languages_json { + // Output as JSON array + let json = serde_json::to_string(&languages).unwrap_or_else(|_| "[]".to_string()); + println!("{}", json); + } else { + // Output one language per line + for lang in &languages { + println!("{}", lang); + } + } + EXIT_SUCCESS + } + Err(e) => { + eprintln!("Error: {}", e); + EXIT_API_ERROR + } + } +} + /// CLI entry point. Call this from main() to run the CLI. /// /// # Examples @@ -3544,7 +3852,9 @@ pub fn cli_main() -> i32 { Some("session") => cmd_session(&opts), Some("service") => cmd_service(&opts), Some("snapshot") => cmd_snapshot(&opts), + Some("image") => cmd_image(&opts), Some("key") => cmd_key(&opts), + Some("languages") => cmd_languages(&opts), None => { // Default: execute code if opts.positional.is_empty() && opts.shell.is_none() { diff --git a/clients/scheme/sync/src/un.scm b/clients/scheme/sync/src/un.scm index 1940404..bf6161b 100644 --- a/clients/scheme/sync/src/un.scm +++ b/clients/scheme/sync/src/un.scm @@ -382,6 +382,33 @@ (let ((cmd (format #f "xdg-open '~a' 2>/dev/null &" url))) (system cmd))) +(define (json-extract-array json key) + "Extract array values for key from JSON (simple parser)" + ;; This extracts items from a JSON array like "languages":["python","javascript",...] + (let* ((pattern (format #f "\"~a\":\\s*\\[([^\\]]*)\\]" key)) + (cmd (format #f "echo '~a' | grep -oP '\"~a\":\\s*\\[[^]]*\\]' | sed 's/\"~a\":\\s*\\[//' | sed 's/\\]//' | tr ',' '\\n' | sed 's/\"//g' | sed 's/^ *//' | sed 's/ *$//'" json key key)) + (port (open-input-pipe cmd)) + (result (let loop ((lines '())) + (let ((line (read-line port))) + (if (eof-object? line) + (reverse lines) + (loop (cons (string-trim-both line) lines))))))) + (close-pipe port) + (filter (lambda (s) (> (string-length s) 0)) result))) + +(define (languages-cmd json-output) + (let* ((api-key (get-api-key)) + (response (curl-get api-key "/languages")) + (langs (json-extract-array response "languages"))) + (if json-output + ;; JSON array output + (format #t "[~a]\n" (string-join (map (lambda (l) (format #f "\"~a\"" l)) langs) ",")) + ;; One language per line (default) + (for-each (lambda (lang) + (display lang) + (newline)) + langs)))) + (define (validate-key-cmd extend) (let* ((api-key (get-api-key)) (response (curl-post-portal api-key "/keys/validate" "{}")) @@ -556,6 +583,68 @@ (display "Error: --name required to create service, or use env subcommand\n" (current-error-port)) (exit 1))))) +(define (image-cmd action id source-type visibility-mode name ports) + (let ((api-key (get-api-key))) + (cond + ((equal? action "list") + (let* ((response (curl-get api-key "/images")) + (images (json-extract-array response "images"))) + (if (null? images) + (display "No images found\n") + (begin + (format #t "~a~a ~a ~a ~a~a\n" blue "ID" "Name" "Visibility" "Created" reset) + (display response) + (newline))))) + ((equal? action "info") + (display (curl-get api-key (format #f "/images/~a" id))) + (newline)) + ((equal? action "delete") + (curl-delete api-key (format #f "/images/~a" id)) + (format #t "~aImage deleted successfully~a\n" green reset)) + ((equal? action "lock") + (curl-post api-key (format #f "/images/~a/lock" id) "{}") + (format #t "~aImage locked successfully~a\n" green reset)) + ((equal? action "unlock") + (curl-post api-key (format #f "/images/~a/unlock" id) "{}") + (format #t "~aImage unlocked successfully~a\n" green reset)) + ((equal? action "publish") + (if (not source-type) + (begin + (format (current-error-port) "~aError: --source-type required for --publish (service or snapshot)~a\n" red reset) + (exit 1)) + (let* ((name-json (if name (format #f ",\"name\":\"~a\"" (escape-json name)) "")) + (json (format #f "{\"source_type\":\"~a\",\"source_id\":\"~a\"~a}" source-type id name-json)) + (response (curl-post api-key "/images/publish" json)) + (image-id (json-extract-string response "id"))) + (format #t "~aImage published successfully~a\n" green reset) + (format #t "Image ID: ~a\n" (or image-id "N/A"))))) + ((equal? action "visibility") + (if (not visibility-mode) + (begin + (format (current-error-port) "~aError: visibility mode required (private, unlisted, or public)~a\n" red reset) + (exit 1)) + (let ((json (format #f "{\"visibility\":\"~a\"}" visibility-mode))) + (curl-post api-key (format #f "/images/~a/visibility" id) json) + (format #t "~aImage visibility set to ~a~a\n" green visibility-mode reset)))) + ((equal? action "spawn") + (let* ((name-json (if name (format #f "\"name\":\"~a\"" (escape-json name)) "")) + (ports-json (if ports (format #f "~a\"ports\":[~a]" (if name "," "") ports) "")) + (json (format #f "{~a~a}" name-json ports-json)) + (response (curl-post api-key (format #f "/images/~a/spawn" id) json)) + (service-id (json-extract-string response "id"))) + (format #t "~aService spawned from image~a\n" green reset) + (format #t "Service ID: ~a\n" (or service-id "N/A")))) + ((equal? action "clone") + (let* ((name-json (if name (format #f "\"name\":\"~a\"" (escape-json name)) "")) + (json (format #f "{~a}" name-json)) + (response (curl-post api-key (format #f "/images/~a/clone" id) json)) + (image-id (json-extract-string response "id"))) + (format #t "~aImage cloned successfully~a\n" green reset) + (format #t "Image ID: ~a\n" (or image-id "N/A")))) + (else + (display "Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID\n" (current-error-port)) + (exit 1))))) + (define (parse-input-files args) "Parse -f flags from args and return list of filenames" (let loop ((args args) (files '())) @@ -576,12 +665,93 @@ (display "Usage: un.scm [options] \n") (display " un.scm session [options]\n") (display " un.scm service [options]\n") + (display " un.scm image [options]\n") (display " un.scm key [--extend]\n") + (display " un.scm languages [--json]\n") + (display "\nLanguages options:\n") + (display " --json Output as JSON array\n") + (display "\nImage options:\n") + (display " --list List all images\n") + (display " --info ID Get image details\n") + (display " --delete ID Delete an image\n") + (display " --lock ID Lock image to prevent deletion\n") + (display " --unlock ID Unlock image\n") + (display " --publish ID Publish image from service/snapshot\n") + (display " --source-type TYPE Source type: service or snapshot\n") + (display " --visibility ID MODE Set visibility: private, unlisted, public\n") + (display " --spawn ID Spawn new service from image\n") + (display " --clone ID Clone an image\n") + (display " --name NAME Name for spawned service or cloned image\n") + (display " --ports PORTS Ports for spawned service\n") (exit 1)) (cond + ((equal? (car args) "languages") + (let ((json-output (and (> (length args) 1) (equal? (cadr args) "--json")))) + (languages-cmd json-output))) ((equal? (car args) "key") (let ((extend (and (> (length args) 1) (equal? (cadr args) "--extend")))) (validate-key-cmd extend))) + ((equal? (car args) "image") + (cond + ((and (> (length args) 1) (equal? (cadr args) "--list")) + (image-cmd "list" #f #f #f #f #f)) + ((and (> (length args) 2) (equal? (cadr args) "--info")) + (image-cmd "info" (caddr args) #f #f #f #f)) + ((and (> (length args) 2) (equal? (cadr args) "--delete")) + (image-cmd "delete" (caddr args) #f #f #f #f)) + ((and (> (length args) 2) (equal? (cadr args) "--lock")) + (image-cmd "lock" (caddr args) #f #f #f #f)) + ((and (> (length args) 2) (equal? (cadr args) "--unlock")) + (image-cmd "unlock" (caddr args) #f #f #f #f)) + ((and (> (length args) 2) (equal? (cadr args) "--publish")) + (let* ((publish-id (caddr args)) + (rest-args (cdddr args)) + (source-type #f) + (name #f)) + (let loop ((args rest-args)) + (when (pair? args) + (cond + ((and (equal? (car args) "--source-type") (pair? (cdr args))) + (set! source-type (cadr args)) + (loop (cddr args))) + ((and (equal? (car args) "--name") (pair? (cdr args))) + (set! name (cadr args)) + (loop (cddr args))) + (else (loop (cdr args)))))) + (image-cmd "publish" publish-id source-type #f name #f))) + ((and (> (length args) 3) (equal? (cadr args) "--visibility")) + (image-cmd "visibility" (caddr args) #f (list-ref args 3) #f #f)) + ((and (> (length args) 2) (equal? (cadr args) "--spawn")) + (let* ((spawn-id (caddr args)) + (rest-args (cdddr args)) + (name #f) + (ports #f)) + (let loop ((args rest-args)) + (when (pair? args) + (cond + ((and (equal? (car args) "--name") (pair? (cdr args))) + (set! name (cadr args)) + (loop (cddr args))) + ((and (equal? (car args) "--ports") (pair? (cdr args))) + (set! ports (cadr args)) + (loop (cddr args))) + (else (loop (cdr args)))))) + (image-cmd "spawn" spawn-id #f #f name ports))) + ((and (> (length args) 2) (equal? (cadr args) "--clone")) + (let* ((clone-id (caddr args)) + (rest-args (cdddr args)) + (name #f)) + (let loop ((args rest-args)) + (when (pair? args) + (cond + ((and (equal? (car args) "--name") (pair? (cdr args))) + (set! name (cadr args)) + (loop (cddr args))) + (else (loop (cdr args)))))) + (image-cmd "clone" clone-id #f #f name #f))) + (else + (display "Error: Invalid image command\n" (current-error-port)) + (exit 1)))) ((equal? (car args) "session") (if (and (> (length args) 1) (equal? (cadr args) "--list")) (session-cmd "list" #f #f '()) diff --git a/clients/swift/sync/src/un.swift b/clients/swift/sync/src/un.swift index ec4dba5..0fb9cb6 100644 --- a/clients/swift/sync/src/un.swift +++ b/clients/swift/sync/src/un.swift @@ -1176,6 +1176,7 @@ func printHelp() { un service-env Manage service environment un snapshot [options] Manage snapshots un key Check API key + un languages [--json] List available languages GLOBAL OPTIONS: -s, --shell LANG Language for inline code @@ -1305,10 +1306,20 @@ class CLIArgs { var clone: String? var cloneType: String? + // Image options + var publish: String? + var sourceType: String? + var visibility: String? + var visibilityMode: String? + var spawn: String? + // Service-env var serviceEnvAction: String? var serviceEnvId: String? + // Languages options + var jsonOutput: Bool = false + func parse(_ args: [String]) { var i = 0 let args = Array(args.dropFirst()) // Skip program name @@ -1351,6 +1362,8 @@ class CLIArgs { if i < args.count { vcpu = Int(args[i]) ?? 1 } case "-y", "--yes": yes = true + case "--json": + jsonOutput = true case "-l", "--list": listFlag = true case "--attach": @@ -1448,7 +1461,24 @@ class CLIArgs { case "--clone": i += 1 if i < args.count { clone = args[i] } - case "session", "service", "snapshot", "key": + case "--publish": + i += 1 + if i < args.count { publish = args[i] } + case "--source-type": + i += 1 + if i < args.count { sourceType = args[i] } + case "--visibility": + i += 1 + if i < args.count { visibility = args[i] } + // Check if next arg is the mode (not a flag) + if i + 1 < args.count && !args[i + 1].hasPrefix("-") { + i += 1 + visibilityMode = args[i] + } + case "--spawn": + i += 1 + if i < args.count { spawn = args[i] } + case "session", "service", "snapshot", "image", "key", "languages": command = arg case "service-env": command = "service-env" @@ -1460,7 +1490,7 @@ class CLIArgs { if arg.hasPrefix("-") { fputs("Error: Unknown option \(arg)\n", stderr) exit(2) - } else if command == nil && (arg == "session" || arg == "service" || arg == "snapshot" || arg == "key" || arg == "service-env") { + } else if command == nil && (arg == "session" || arg == "service" || arg == "snapshot" || arg == "image" || arg == "key" || arg == "service-env" || arg == "languages") { command = arg } else if source == nil { source = arg @@ -1809,6 +1839,96 @@ func handleSnapshotCommand(_ args: CLIArgs, _ pk: String, _ sk: String) throws { } } +/// Handle image command +func handleImageCommand(_ args: CLIArgs, _ pk: String, _ sk: String) throws { + if args.listFlag { + let response = try listImages(publicKey: pk, secretKey: sk) + // Extract images array from response + let images = response["images"] as? [[String: Any]] ?? [] + print(formatImageListOutput(images)) + } else if let infoId = args.info { + let image = try getImage(infoId, publicKey: pk, secretKey: sk) + let jsonData = try JSONSerialization.data(withJSONObject: image, options: .prettyPrinted) + print(String(data: jsonData, encoding: .utf8) ?? "{}") + } else if let deleteId = args.delete { + _ = try deleteImage(deleteId, publicKey: pk, secretKey: sk) + print("Image \(deleteId) deleted") + } else if let lockId = args.lock { + _ = try lockImage(lockId, publicKey: pk, secretKey: sk) + print("Image \(lockId) locked") + } else if let unlockId = args.unlock { + _ = try unlockImage(unlockId, publicKey: pk, secretKey: sk) + print("Image \(unlockId) unlocked") + } else if let publishId = args.publish { + guard let sourceType = args.sourceType else { + fputs("Error: --source-type required for --publish\n", stderr) + exit(2) + } + let result = try imagePublish( + sourceType: sourceType, + sourceId: publishId, + name: args.name, + publicKey: pk, + secretKey: sk + ) + let imageId = result["image_id"] as? String ?? result["id"] as? String ?? "" + print("Image published: \(imageId)") + } else if let visId = args.visibility, let mode = args.visibilityMode { + if mode != "private" && mode != "unlisted" && mode != "public" { + fputs("Error: visibility must be private, unlisted, or public\n", stderr) + exit(2) + } + _ = try setImageVisibility(visId, visibility: mode, publicKey: pk, secretKey: sk) + print("Image \(visId) visibility set to \(mode)") + } else if let spawnId = args.spawn { + guard let name = args.name else { + fputs("Error: --name required for --spawn\n", stderr) + exit(2) + } + var ports: [Int]? = nil + if let portsStr = args.ports { + ports = portsStr.components(separatedBy: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) } + } + let result = try spawnFromImage( + spawnId, + name: name, + ports: ports, + publicKey: pk, + secretKey: sk + ) + let serviceId = result["service_id"] as? String ?? result["id"] as? String ?? "" + print("Service spawned: \(serviceId)") + } else if let cloneId = args.clone { + let result = try cloneImage(cloneId, name: args.name, publicKey: pk, secretKey: sk) + let imageId = result["image_id"] as? String ?? result["id"] as? String ?? "" + print("Image cloned: \(imageId)") + } else { + fputs("Error: No action specified for image command\n", stderr) + exit(2) + } +} + +/// Format image list output +func formatImageListOutput(_ images: [[String: Any]]) -> String { + if images.isEmpty { + return "No images found." + } + + var lines: [String] = [] + lines.append(String(format: "%-38s %-20s %-10s %-10s %s", "ID", "NAME", "VISIBILITY", "SOURCE", "CREATED")) + + for image in images { + let id = (image["image_id"] as? String ?? image["id"] as? String ?? "-").prefix(38) + let name = (image["name"] as? String ?? "-").prefix(20) + let visibility = (image["visibility"] as? String ?? "private").prefix(10) + let sourceType = (image["source_type"] as? String ?? "-").prefix(10) + let createdAt = (image["created_at"] as? String ?? "-").prefix(19) + lines.append(String(format: "%-38s %-20s %-10s %-10s %s", String(id), String(name), String(visibility), String(sourceType), String(createdAt))) + } + + return lines.joined(separator: "\n") +} + /// Handle key command func handleKeyCommand(_ pk: String, _ sk: String) throws { let result = try validateKeys(publicKey: pk, secretKey: sk) @@ -1826,6 +1946,24 @@ func handleKeyCommand(_ pk: String, _ sk: String) throws { } } +/// Handle languages command +func handleLanguagesCommand(_ args: CLIArgs, _ pk: String, _ sk: String) throws { + let languages = try getLanguages(publicKey: pk, secretKey: sk) + + if args.jsonOutput { + // Output as JSON array + if let jsonData = try? JSONSerialization.data(withJSONObject: languages), + let jsonString = String(data: jsonData, encoding: .utf8) { + print(jsonString) + } + } else { + // Output one language per line (pipe-friendly) + for lang in languages { + print(lang) + } + } +} + /// Main CLI entry point @main struct UnCLI { @@ -1854,8 +1992,12 @@ struct UnCLI { try handleServiceEnvCommand(args, pk, sk) case "snapshot": try handleSnapshotCommand(args, pk, sk) + case "image": + try handleImageCommand(args, pk, sk) case "key": try handleKeyCommand(pk, sk) + case "languages": + try handleLanguagesCommand(args, pk, sk) default: if args.source != nil || args.shell != nil { try handleExecuteCommand(args, pk, sk) diff --git a/clients/tcl/sync/src/un.tcl b/clients/tcl/sync/src/un.tcl index 04a12c1..12aa9c2 100755 --- a/clients/tcl/sync/src/un.tcl +++ b/clients/tcl/sync/src/un.tcl @@ -573,6 +573,36 @@ proc cmd_session {args} { puts "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}" } +proc cmd_languages {args} { + lassign [get_api_keys] public_key secret_key + set json_output 0 + + # Parse arguments + for {set i 0} {$i < [llength $args]} {incr i} { + set arg [lindex $args $i] + if {$arg eq "--json"} { + set json_output 1 + } + } + + set result [api_request "/languages" "GET" {} $public_key $secret_key] + set langs [dict get $result languages] + + if {$json_output} { + # JSON array output + set json_langs [list] + foreach lang $langs { + lappend json_langs [::json::write string $lang] + } + puts [::json::write array {*}$json_langs] + } else { + # One language per line (default) + foreach lang $langs { + puts $lang + } + } +} + proc cmd_key {args} { lassign [get_api_keys] public_key secret_key set extend_mode 0 @@ -667,6 +697,192 @@ proc cmd_key {args} { } } +proc cmd_image {args} { + lassign [get_api_keys] public_key secret_key + set list_mode 0 + set info_id "" + set delete_id "" + set lock_id "" + set unlock_id "" + set publish_id "" + set source_type "" + set visibility_id "" + set visibility_mode "" + set spawn_id "" + set clone_id "" + set name "" + set ports "" + + # Parse arguments + for {set i 0} {$i < [llength $args]} {incr i} { + set arg [lindex $args $i] + switch -exact -- $arg { + --list { + set list_mode 1 + } + -l { + set list_mode 1 + } + --info { + incr i + set info_id [lindex $args $i] + } + --delete { + incr i + set delete_id [lindex $args $i] + } + --lock { + incr i + set lock_id [lindex $args $i] + } + --unlock { + incr i + set unlock_id [lindex $args $i] + } + --publish { + incr i + set publish_id [lindex $args $i] + } + --source-type { + incr i + set source_type [lindex $args $i] + } + --visibility { + incr i + set visibility_id [lindex $args $i] + incr i + if {$i < [llength $args] && [string index [lindex $args $i] 0] ne "-"} { + set visibility_mode [lindex $args $i] + } else { + incr i -1 + } + } + --spawn { + incr i + set spawn_id [lindex $args $i] + } + --clone { + incr i + set clone_id [lindex $args $i] + } + --name { + incr i + set name [lindex $args $i] + } + --ports { + incr i + set ports [lindex $args $i] + } + } + } + + if {$list_mode} { + set result [api_request "/images" "GET" {} $public_key $secret_key] + set images [dict get $result images] + if {[llength $images] == 0} { + puts "No images found" + } else { + puts [format "%-40s %-20s %-12s %s" "ID" "Name" "Visibility" "Created"] + foreach img $images { + puts [format "%-40s %-20s %-12s %s" \ + [dict get $img id] \ + [expr {[dict exists $img name] ? [dict get $img name] : "-"}] \ + [dict get $img visibility] \ + [dict get $img created_at]] + } + } + return + } + + if {$info_id ne ""} { + set result [api_request "/images/$info_id" "GET" {} $public_key $secret_key] + puts "${::BLUE}Image Details${::RESET}" + puts "" + puts "Image ID: [dict get $result id]" + puts "Name: [expr {[dict exists $result name] ? [dict get $result name] : \"-\"}]" + puts "Visibility: [dict get $result visibility]" + puts "Created: [dict get $result created_at]" + return + } + + if {$delete_id ne ""} { + api_request "/images/$delete_id" "DELETE" {} $public_key $secret_key + puts "${::GREEN}Image deleted successfully${::RESET}" + return + } + + if {$lock_id ne ""} { + api_request "/images/$lock_id/lock" "POST" {} $public_key $secret_key + puts "${::GREEN}Image locked successfully${::RESET}" + return + } + + if {$unlock_id ne ""} { + api_request "/images/$unlock_id/unlock" "POST" {} $public_key $secret_key + puts "${::GREEN}Image unlocked successfully${::RESET}" + return + } + + if {$publish_id ne ""} { + if {$source_type eq ""} { + puts stderr "${::RED}Error: --source-type required for --publish (service or snapshot)${::RESET}" + exit 1 + } + set payload [list source_type [::json::write string $source_type] source_id [::json::write string $publish_id]] + if {$name ne ""} { + lappend payload name [::json::write string $name] + } + set result [api_request "/images/publish" "POST" $payload $public_key $secret_key] + puts "${::GREEN}Image published successfully${::RESET}" + puts "Image ID: [dict get $result id]" + return + } + + if {$visibility_id ne ""} { + if {$visibility_mode eq ""} { + puts stderr "${::RED}Error: visibility mode required (private, unlisted, or public)${::RESET}" + exit 1 + } + set payload [list visibility [::json::write string $visibility_mode]] + api_request "/images/$visibility_id/visibility" "POST" $payload $public_key $secret_key + puts "${::GREEN}Image visibility set to $visibility_mode${::RESET}" + return + } + + if {$spawn_id ne ""} { + set payload [list] + if {$name ne ""} { + lappend payload name [::json::write string $name] + } + if {$ports ne ""} { + set port_list [split $ports ","] + set port_json [list] + foreach p $port_list { + lappend port_json $p + } + lappend payload ports [::json::write array {*}$port_json] + } + set result [api_request "/images/$spawn_id/spawn" "POST" $payload $public_key $secret_key] + puts "${::GREEN}Service spawned from image${::RESET}" + puts "Service ID: [dict get $result id]" + return + } + + if {$clone_id ne ""} { + set payload [list] + if {$name ne ""} { + lappend payload name [::json::write string $name] + } + set result [api_request "/images/$clone_id/clone" "POST" $payload $public_key $secret_key] + puts "${::GREEN}Image cloned successfully${::RESET}" + puts "Image ID: [dict get $result id]" + return + } + + puts stderr "${::RED}Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${::RESET}" + exit 1 +} + proc cmd_service {args} { lassign [get_api_keys] public_key secret_key set list_mode 0 @@ -975,7 +1191,12 @@ proc main {argv} { puts stderr " un.tcl session \[options\]" puts stderr " un.tcl service \[options\]" puts stderr " un.tcl service env \[options\]" + puts stderr " un.tcl image \[options\]" puts stderr " un.tcl key \[--extend\]" + puts stderr " un.tcl languages \[--json\]" + puts stderr "" + puts stderr "Languages options:" + puts stderr " --json Output as JSON array" puts stderr "" puts stderr "Service env commands:" puts stderr " env status ID Check vault status" @@ -986,6 +1207,20 @@ proc main {argv} { puts stderr "Service vault options:" puts stderr " -e KEY=VALUE Set vault env var (with --name or env set)" puts stderr " --env-file FILE Load vault vars from file" + puts stderr "" + puts stderr "Image options:" + puts stderr " --list List all images" + puts stderr " --info ID Get image details" + puts stderr " --delete ID Delete an image" + puts stderr " --lock ID Lock image to prevent deletion" + puts stderr " --unlock ID Unlock image" + puts stderr " --publish ID Publish image from service/snapshot" + puts stderr " --source-type TYPE Source type: service or snapshot" + puts stderr " --visibility ID MODE Set visibility: private, unlisted, public" + puts stderr " --spawn ID Spawn new service from image" + puts stderr " --clone ID Clone an image" + puts stderr " --name NAME Name for spawned service or cloned image" + puts stderr " --ports PORTS Ports for spawned service" exit 1 } @@ -995,8 +1230,12 @@ proc main {argv} { cmd_session [lrange $argv 1 end] } elseif {$first_arg eq "service"} { cmd_service [lrange $argv 1 end] + } elseif {$first_arg eq "image"} { + cmd_image [lrange $argv 1 end] } elseif {$first_arg eq "key"} { cmd_key [lrange $argv 1 end] + } elseif {$first_arg eq "languages"} { + cmd_languages [lrange $argv 1 end] } else { cmd_execute $argv } diff --git a/clients/typescript/sync/src/un.ts b/clients/typescript/sync/src/un.ts index 0a54dc3..8c95054 100644 --- a/clients/typescript/sync/src/un.ts +++ b/clients/typescript/sync/src/un.ts @@ -129,6 +129,17 @@ interface Args { envFile: string | null; envAction: string | null; envTarget: string | null; + jsonOutput: boolean; + imageInfo: string | null; + imageDelete: string | null; + imageLock: string | null; + imageUnlock: string | null; + imagePublish: string | null; + sourceType: string | null; + imageVisibility: string | null; + visibilityMode: string | null; + imageSpawn: string | null; + imageClone: string | null; } interface ApiKeys { @@ -790,6 +801,115 @@ async function validateKey(keys: ApiKeys, shouldExtend: boolean): Promise } } +async function cmdLanguages(args: Args): Promise { + const keys = getApiKeys(args.apiKey); + const result = await apiRequest("/languages", "GET", null, keys); + const langs = result.languages || []; + + if (args.jsonOutput) { + // JSON array output + console.log(JSON.stringify(langs)); + } else { + // One language per line (default) + langs.forEach((lang: string) => { + console.log(lang); + }); + } +} + +async function cmdImage(args: Args): Promise { + const keys = getApiKeys(args.apiKey); + + if (args.list) { + const result = await apiRequest("/images", "GET", null, keys); + const images = result.images || []; + if (images.length === 0) { + console.log("No images found"); + } else { + console.log(`${'ID'.padEnd(40)} ${'Name'.padEnd(20)} ${'Visibility'.padEnd(12)} Created`); + images.forEach((img: any) => { + console.log(`${(img.id || 'N/A').padEnd(40)} ${(img.name || '-').padEnd(20)} ${(img.visibility || 'N/A').padEnd(12)} ${img.created_at || 'N/A'}`); + }); + } + return; + } + + if (args.imageInfo) { + const result = await apiRequest(`/images/${args.imageInfo}`, "GET", null, keys); + console.log(`${BLUE}Image Details${RESET}`); + console.log(""); + console.log(`Image ID: ${result.id || 'N/A'}`); + console.log(`Name: ${result.name || '-'}`); + console.log(`Visibility: ${result.visibility || 'N/A'}`); + console.log(`Created: ${result.created_at || 'N/A'}`); + return; + } + + if (args.imageDelete) { + await apiRequest(`/images/${args.imageDelete}`, "DELETE", null, keys); + console.log(`${GREEN}Image deleted successfully${RESET}`); + return; + } + + if (args.imageLock) { + await apiRequest(`/images/${args.imageLock}/lock`, "POST", {}, keys); + console.log(`${GREEN}Image locked successfully${RESET}`); + return; + } + + if (args.imageUnlock) { + await apiRequest(`/images/${args.imageUnlock}/unlock`, "POST", {}, keys); + console.log(`${GREEN}Image unlocked successfully${RESET}`); + return; + } + + if (args.imagePublish) { + if (!args.sourceType) { + console.error(`${RED}Error: --source-type required for --publish (service or snapshot)${RESET}`); + process.exit(1); + } + const payload: any = { source_type: args.sourceType, source_id: args.imagePublish }; + if (args.name) payload.name = args.name; + const result = await apiRequest("/images/publish", "POST", payload, keys); + console.log(`${GREEN}Image published successfully${RESET}`); + console.log(`Image ID: ${result.id || 'N/A'}`); + return; + } + + if (args.imageVisibility) { + if (!args.visibilityMode) { + console.error(`${RED}Error: visibility mode required (private, unlisted, or public)${RESET}`); + process.exit(1); + } + const payload = { visibility: args.visibilityMode }; + await apiRequest(`/images/${args.imageVisibility}/visibility`, "POST", payload, keys); + console.log(`${GREEN}Image visibility set to ${args.visibilityMode}${RESET}`); + return; + } + + if (args.imageSpawn) { + const payload: any = {}; + if (args.name) payload.name = args.name; + if (args.ports) payload.ports = args.ports.split(',').map(p => parseInt(p.trim())); + const result = await apiRequest(`/images/${args.imageSpawn}/spawn`, "POST", payload, keys); + console.log(`${GREEN}Service spawned from image${RESET}`); + console.log(`Service ID: ${result.id || 'N/A'}`); + return; + } + + if (args.imageClone) { + const payload: any = {}; + if (args.name) payload.name = args.name; + const result = await apiRequest(`/images/${args.imageClone}/clone`, "POST", payload, keys); + console.log(`${GREEN}Image cloned successfully${RESET}`); + console.log(`Image ID: ${result.id || 'N/A'}`); + return; + } + + console.error(`${RED}Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${RESET}`); + process.exit(1); +} + async function cmdKey(args: Args): Promise { const keys = getApiKeys(args.apiKey); await validateKey(keys, args.extend); @@ -834,15 +954,29 @@ function parseArgs(argv: string[]): Args { envFile: null, envAction: null, envTarget: null, + jsonOutput: false, + imageInfo: null, + imageDelete: null, + imageLock: null, + imageUnlock: null, + imagePublish: null, + sourceType: null, + imageVisibility: null, + visibilityMode: null, + imageSpawn: null, + imageClone: null, }; let i = 2; while (i < argv.length) { const arg = argv[i]; - if (arg === 'session' || arg === 'service' || arg === 'key') { + if (arg === 'session' || arg === 'service' || arg === 'key' || arg === 'languages' || arg === 'image') { args.command = arg; i++; + } else if (arg === '--json') { + args.jsonOutput = true; + i++; } else if (arg === '-e' && i + 1 < argv.length) { args.env.push(argv[++i]); i++; @@ -918,7 +1052,38 @@ function parseArgs(argv: string[]): Args { } i++; } else if (arg === '--info' && i + 1 < argv.length) { - args.info = argv[++i]; + if (args.command === 'image') { + args.imageInfo = argv[++i]; + } else { + args.info = argv[++i]; + } + i++; + } else if (arg === '--delete' && i + 1 < argv.length) { + if (args.command === 'image') { + args.imageDelete = argv[++i]; + } + i++; + } else if (arg === '--lock' && i + 1 < argv.length) { + args.imageLock = argv[++i]; + i++; + } else if (arg === '--unlock' && i + 1 < argv.length) { + args.imageUnlock = argv[++i]; + i++; + } else if (arg === '--publish' && i + 1 < argv.length) { + args.imagePublish = argv[++i]; + i++; + } else if (arg === '--source-type' && i + 1 < argv.length) { + args.sourceType = argv[++i]; + i++; + } else if (arg === '--visibility' && i + 1 < argv.length) { + args.imageVisibility = argv[++i]; + i++; + if (i < argv.length && !argv[i].startsWith('-')) { + args.visibilityMode = argv[i]; + i++; + } + } else if (arg === '--spawn' && i + 1 < argv.length) { + args.imageSpawn = argv[++i]; i++; } else if (arg === '--logs' && i + 1 < argv.length) { args.logs = argv[++i]; @@ -953,6 +1118,13 @@ function parseArgs(argv: string[]): Args { } else if (arg === '--extend') { args.extend = true; i++; + } else if (arg === '--clone' && i + 1 < argv.length) { + if (args.command === 'image') { + args.imageClone = argv[++i]; + } else { + args.clone = argv[++i]; + } + i++; } else if (!arg.startsWith('-')) { args.sourceFile = arg; i++; @@ -978,8 +1150,12 @@ async function main(): Promise { } else { await cmdService(args); } + } else if (args.command === 'image') { + await cmdImage(args); } else if (args.command === 'key') { await cmdKey(args); + } else if (args.command === 'languages') { + await cmdLanguages(args); } else if (args.sourceFile) { await cmdExecute(args); } else { @@ -989,7 +1165,9 @@ Usage: ${process.argv[1]} [options] ${process.argv[1]} session [options] ${process.argv[1]} service [options] + ${process.argv[1]} image [options] ${process.argv[1]} key [options] + ${process.argv[1]} languages [--json] Execute options: -e KEY=VALUE Environment variable (multiple allowed) @@ -1029,8 +1207,25 @@ Service options: --dump-bootstrap ID Dump bootstrap script --dump-file FILE File to save bootstrap (with --dump-bootstrap) +Image options: + --list List all images + --info ID Get image details + --delete ID Delete an image + --lock ID Lock image to prevent deletion + --unlock ID Unlock image + --publish ID Publish image from service/snapshot + --source-type TYPE Source type: service or snapshot + --visibility ID MODE Set visibility: private, unlisted, public + --spawn ID Spawn new service from image + --clone ID Clone an image + --name NAME Name for spawned service or cloned image + --ports PORTS Ports for spawned service + Key options: --extend Open browser to extend key expiration + +Languages options: + --json Output as JSON array `); process.exit(1); } diff --git a/clients/v/sync/src/un.v b/clients/v/sync/src/un.v index 092f7dd..bd17ae3 100644 --- a/clients/v/sync/src/un.v +++ b/clients/v/sync/src/un.v @@ -594,6 +594,152 @@ fn get_secret_key() string { return '' } +fn cmd_image(list bool, info string, delete string, lock string, unlock string, publish string, source_type string, visibility_id string, visibility_mode string, spawn string, clone string, name string, ports string, api_key string) { + pub_key := get_public_key() + secret_key := get_secret_key() + + if list { + cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/images:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/images' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\"" + println(exec_curl(cmd)) + return + } + + if info != '' { + cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/images/${info}:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/images/${info}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\"" + println(exec_curl(cmd)) + return + } + + if delete != '' { + cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:DELETE:/images/${delete}:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X DELETE '${api_base}/images/${delete}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\"" + exec_curl(cmd) + println('${green}Image deleted: ${delete}${reset}') + return + } + + if lock != '' { + cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${lock}/lock:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${lock}/lock' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\"" + exec_curl(cmd) + println('${green}Image locked: ${lock}${reset}') + return + } + + if unlock != '' { + cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${unlock}/unlock:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${unlock}/unlock' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\"" + exec_curl(cmd) + println('${green}Image unlocked: ${unlock}${reset}') + return + } + + if publish != '' { + if source_type == '' { + eprintln('${red}Error: --publish requires --source-type (service or snapshot)${reset}') + exit(1) + } + mut json := '{"source_type":"${source_type}","source_id":"${publish}"' + if name != '' { + json += ',"name":"${name}"' + } + json += '}' + cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/publish:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/publish' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\"" + result := exec_curl(cmd) + println(result) + return + } + + if visibility_id != '' { + if visibility_mode == '' { + eprintln('${red}Error: --visibility requires a mode (private, unlisted, or public)${reset}') + exit(1) + } + json := '{"visibility":"${visibility_mode}"}' + cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${visibility_id}/visibility:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${visibility_id}/visibility' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\"" + exec_curl(cmd) + println('${green}Image visibility set to ${visibility_mode}: ${visibility_id}${reset}') + return + } + + if spawn != '' { + mut json := '{"name":"${name}"' + if ports != '' { + json += ',"ports":[${ports}]' + } + json += '}' + cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${spawn}/spawn:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${spawn}/spawn' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\"" + result := exec_curl(cmd) + println(result) + return + } + + if clone != '' { + mut json := '{' + if name != '' { + json += '"name":"${name}"' + } + json += '}' + cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${clone}/clone:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${clone}/clone' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\"" + result := exec_curl(cmd) + println(result) + return + } + + eprintln('${red}Error: No image action specified. Use --list, --info, --delete, --publish, etc.${reset}') + exit(1) +} + +fn cmd_languages(json_output bool, api_key string) { + pub_key := get_public_key() + secret_key := get_secret_key() + + cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/languages:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/languages' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\"" + result := exec_curl(cmd) + + if json_output { + // Extract languages array and print as JSON + // Find the languages array in the response + start_idx := result.index('"languages":[') or { + println(result) + return + } + arr_start := result.index_after('[', start_idx) + if arr_start < 0 { + println(result) + return + } + arr_end := result.index_after(']', arr_start) + if arr_end < 0 { + println(result) + return + } + println(result[arr_start..arr_end + 1]) + } else { + // Parse languages array and print one per line + start_idx := result.index('"languages":[') or { + println(result) + return + } + arr_start := result.index_after('[', start_idx) + if arr_start < 0 { + println(result) + return + } + arr_end := result.index_after(']', arr_start) + if arr_end < 0 { + println(result) + return + } + // Extract array content and parse + arr_content := result[arr_start + 1..arr_end] + // Split by comma and extract language names + for item in arr_content.split(',') { + lang := item.trim_space().trim('"') + if lang.len > 0 { + println(lang) + } + } + } +} + fn main() { mut api_key := get_public_key() @@ -602,13 +748,26 @@ fn main() { eprintln(' ${os.args[0]} session [options]') eprintln(' ${os.args[0]} service [options]') eprintln(' ${os.args[0]} service env [options]') + eprintln(' ${os.args[0]} image [options]') eprintln(' ${os.args[0]} key [--extend]') + eprintln(' ${os.args[0]} languages [--json]') eprintln('') eprintln('Vault commands:') eprintln(' service env status Check vault status') eprintln(' service env set Set vault (-e KEY=VAL or --env-file FILE)') eprintln(' service env export Export vault contents') eprintln(' service env delete Delete vault') + eprintln('') + eprintln('Image commands:') + eprintln(' image --list List all images') + eprintln(' image --info ID Get image details') + eprintln(' image --delete ID Delete an image') + eprintln(' image --lock ID Lock image to prevent deletion') + eprintln(' image --unlock ID Unlock image') + eprintln(' image --publish ID --source-type TYPE Publish from service/snapshot') + eprintln(' image --visibility ID MODE Set visibility (private/unlisted/public)') + eprintln(' image --spawn ID --name NAME Spawn service from image') + eprintln(' image --clone ID --name NAME Clone an image') exit(1) } @@ -836,6 +995,106 @@ fn main() { return } + if os.args[1] == 'languages' { + mut json_output := false + + mut i := 2 + for i < os.args.len { + match os.args[i] { + '--json' { json_output = true } + '-k' { + i++ + api_key = os.args[i] + } + else {} + } + i++ + } + + cmd_languages(json_output, api_key) + return + } + + if os.args[1] == 'image' { + mut list := false + mut info := '' + mut delete := '' + mut lock := '' + mut unlock := '' + mut publish := '' + mut source_type := '' + mut visibility_id := '' + mut visibility_mode := '' + mut spawn := '' + mut clone := '' + mut name := '' + mut ports := '' + + mut i := 2 + for i < os.args.len { + match os.args[i] { + '--list', '-l' { list = true } + '--info' { + i++ + info = os.args[i] + } + '--delete' { + i++ + delete = os.args[i] + } + '--lock' { + i++ + lock = os.args[i] + } + '--unlock' { + i++ + unlock = os.args[i] + } + '--publish' { + i++ + publish = os.args[i] + } + '--source-type' { + i++ + source_type = os.args[i] + } + '--visibility' { + i++ + visibility_id = os.args[i] + i++ + if i < os.args.len { + visibility_mode = os.args[i] + } + } + '--spawn' { + i++ + spawn = os.args[i] + } + '--clone' { + i++ + clone = os.args[i] + } + '--name' { + i++ + name = os.args[i] + } + '--ports' { + i++ + ports = os.args[i] + } + '-k' { + i++ + api_key = os.args[i] + } + else {} + } + i++ + } + + cmd_image(list, info, delete, lock, unlock, publish, source_type, visibility_id, visibility_mode, spawn, clone, name, ports, api_key) + return + } + // Execute mode mut envs := []string{} mut artifacts := false diff --git a/clients/zig/sync/src/un.zig b/clients/zig/sync/src/un.zig index b8bc0fd..23f8d43 100644 --- a/clients/zig/sync/src/un.zig +++ b/clients/zig/sync/src/un.zig @@ -316,12 +316,24 @@ pub fn main() !u8 { std.debug.print(" {s} session [options]\n", .{args[0]}); std.debug.print(" {s} service [options]\n", .{args[0]}); std.debug.print(" {s} service env [options]\n", .{args[0]}); + std.debug.print(" {s} image [options]\n", .{args[0]}); std.debug.print(" {s} key [--extend]\n", .{args[0]}); + std.debug.print(" {s} languages [--json]\n", .{args[0]}); std.debug.print("\nVault commands:\n", .{}); std.debug.print(" service env status Check vault status\n", .{}); std.debug.print(" service env set Set vault (-e KEY=VAL or --env-file FILE)\n", .{}); std.debug.print(" service env export Export vault contents\n", .{}); std.debug.print(" service env delete Delete vault\n", .{}); + std.debug.print("\nImage commands:\n", .{}); + std.debug.print(" image --list List all images\n", .{}); + std.debug.print(" image --info ID Get image details\n", .{}); + std.debug.print(" image --delete ID Delete an image\n", .{}); + std.debug.print(" image --lock ID Lock image to prevent deletion\n", .{}); + std.debug.print(" image --unlock ID Unlock image\n", .{}); + std.debug.print(" image --publish ID --source-type TYPE Publish from service/snapshot\n", .{}); + std.debug.print(" image --visibility ID MODE Set visibility (private/unlisted/public)\n", .{}); + std.debug.print(" image --spawn ID --name NAME Spawn service from image\n", .{}); + std.debug.print(" image --clone ID --name NAME Clone an image\n", .{}); return 1; } @@ -707,6 +719,243 @@ pub fn main() !u8 { return 0; } + // Handle languages command + if (mem.eql(u8, args[1], "languages")) { + var json_output = false; + var i: usize = 2; + while (i < args.len) : (i += 1) { + if (mem.eql(u8, args[i], "--json")) { + json_output = true; + } else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) { + i += 1; + allocator.free(public_key); + public_key = try allocator.dupe(u8, args[i]); + } + } + + // Fetch languages from API + const json_file = "/tmp/unsandbox_languages.json"; + const auth_headers = try buildAuthCmd(allocator, "GET", "/languages", "", public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/languages' {s} -o {s}", .{ API_BASE, auth_headers, json_file }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + + // Read the JSON response + const json_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| { + std.debug.print("{s}Error reading languages response: {}{s}\n", .{ RED, err, RESET }); + std.fs.cwd().deleteFile(json_file) catch {}; + return 1; + }; + defer allocator.free(json_content); + std.fs.cwd().deleteFile(json_file) catch {}; + + if (json_output) { + // Find and print just the languages array + const arr_prefix = "\"languages\":["; + if (mem.indexOf(u8, json_content, arr_prefix)) |start_idx| { + const arr_start = start_idx + arr_prefix.len - 1; // Include the '[' + if (mem.indexOfPos(u8, json_content, arr_start, "]")) |end_idx| { + std.debug.print("{s}\n", .{json_content[arr_start .. end_idx + 1]}); + } else { + std.debug.print("{s}\n", .{json_content}); + } + } else { + std.debug.print("{s}\n", .{json_content}); + } + } else { + // Parse and print one language per line + const arr_prefix = "\"languages\":["; + if (mem.indexOf(u8, json_content, arr_prefix)) |start_idx| { + const arr_start = start_idx + arr_prefix.len; + if (mem.indexOfPos(u8, json_content, arr_start, "]")) |end_idx| { + const arr_content = json_content[arr_start..end_idx]; + // Split by comma and extract language names + var it = mem.splitSequence(u8, arr_content, ","); + while (it.next()) |item| { + // Trim whitespace and quotes + const trimmed = mem.trim(u8, item, &std.ascii.whitespace); + if (trimmed.len > 2 and trimmed[0] == '"') { + // Remove quotes + const lang = trimmed[1 .. trimmed.len - 1]; + std.debug.print("{s}\n", .{lang}); + } + } + } else { + std.debug.print("{s}\n", .{json_content}); + } + } else { + std.debug.print("{s}\n", .{json_content}); + } + } + return 0; + } + + // Handle image command + if (mem.eql(u8, args[1], "image")) { + var list = false; + var info: ?[]const u8 = null; + var delete: ?[]const u8 = null; + var lock: ?[]const u8 = null; + var unlock: ?[]const u8 = null; + var publish: ?[]const u8 = null; + var source_type: ?[]const u8 = null; + var visibility_id: ?[]const u8 = null; + var visibility_mode: ?[]const u8 = null; + var spawn: ?[]const u8 = null; + var clone: ?[]const u8 = null; + var name: ?[]const u8 = null; + var ports: ?[]const u8 = null; + var i: usize = 2; + while (i < args.len) : (i += 1) { + if (mem.eql(u8, args[i], "--list") or mem.eql(u8, args[i], "-l")) { + list = true; + } else if (mem.eql(u8, args[i], "--info") and i + 1 < args.len) { + i += 1; + info = args[i]; + } else if (mem.eql(u8, args[i], "--delete") and i + 1 < args.len) { + i += 1; + delete = args[i]; + } else if (mem.eql(u8, args[i], "--lock") and i + 1 < args.len) { + i += 1; + lock = args[i]; + } else if (mem.eql(u8, args[i], "--unlock") and i + 1 < args.len) { + i += 1; + unlock = args[i]; + } else if (mem.eql(u8, args[i], "--publish") and i + 1 < args.len) { + i += 1; + publish = args[i]; + } else if (mem.eql(u8, args[i], "--source-type") and i + 1 < args.len) { + i += 1; + source_type = args[i]; + } else if (mem.eql(u8, args[i], "--visibility") and i + 2 < args.len) { + i += 1; + visibility_id = args[i]; + i += 1; + visibility_mode = args[i]; + } else if (mem.eql(u8, args[i], "--spawn") and i + 1 < args.len) { + i += 1; + spawn = args[i]; + } else if (mem.eql(u8, args[i], "--clone") and i + 1 < args.len) { + i += 1; + clone = args[i]; + } else if (mem.eql(u8, args[i], "--name") and i + 1 < args.len) { + i += 1; + name = args[i]; + } else if (mem.eql(u8, args[i], "--ports") and i + 1 < args.len) { + i += 1; + ports = args[i]; + } else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) { + i += 1; + allocator.free(public_key); + public_key = try allocator.dupe(u8, args[i]); + } + } + + if (list) { + const auth_headers = try buildAuthCmd(allocator, "GET", "/images", "", public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/images' {s}", .{ API_BASE, auth_headers }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n", .{}); + } else if (info) |inf| { + const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{inf}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "GET", path, "", public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/images/{s}' {s}", .{ API_BASE, inf, auth_headers }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n", .{}); + } else if (delete) |del_id| { + const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{del_id}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "DELETE", path, "", public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}/images/{s}' {s}", .{ API_BASE, del_id, auth_headers }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n{s}Image deleted: {s}{s}\n", .{ GREEN, del_id, RESET }); + } else if (lock) |lock_id| { + const path = try std.fmt.allocPrint(allocator, "/images/{s}/lock", .{lock_id}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "POST", path, "", public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/lock' {s}", .{ API_BASE, lock_id, auth_headers }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n{s}Image locked: {s}{s}\n", .{ GREEN, lock_id, RESET }); + } else if (unlock) |unlock_id| { + const path = try std.fmt.allocPrint(allocator, "/images/{s}/unlock", .{unlock_id}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "POST", path, "", public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/unlock' {s}", .{ API_BASE, unlock_id, auth_headers }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n{s}Image unlocked: {s}{s}\n", .{ GREEN, unlock_id, RESET }); + } else if (publish) |pub_id| { + if (source_type == null) { + std.debug.print("{s}Error: --publish requires --source-type (service or snapshot){s}\n", .{ RED, RESET }); + return 1; + } + const nm = name orelse ""; + const json = try std.fmt.allocPrint(allocator, "{{\"source_type\":\"{s}\",\"source_id\":\"{s}\",\"name\":\"{s}\"}}", .{ source_type.?, pub_id, nm }); + defer allocator.free(json); + const auth_headers = try buildAuthCmd(allocator, "POST", "/images/publish", json, public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/publish' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n", .{}); + } else if (visibility_id) |vis_id| { + if (visibility_mode == null) { + std.debug.print("{s}Error: --visibility requires a mode (private, unlisted, or public){s}\n", .{ RED, RESET }); + return 1; + } + const json = try std.fmt.allocPrint(allocator, "{{\"visibility\":\"{s}\"}}", .{visibility_mode.?}); + defer allocator.free(json); + const path = try std.fmt.allocPrint(allocator, "/images/{s}/visibility", .{vis_id}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/visibility' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, vis_id, auth_headers, json }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n{s}Image visibility set to {s}: {s}{s}\n", .{ GREEN, visibility_mode.?, vis_id, RESET }); + } else if (spawn) |spawn_id| { + const nm = name orelse ""; + const pt = ports orelse ""; + const json = try std.fmt.allocPrint(allocator, "{{\"name\":\"{s}\",\"ports\":[{s}]}}", .{ nm, pt }); + defer allocator.free(json); + const path = try std.fmt.allocPrint(allocator, "/images/{s}/spawn", .{spawn_id}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/spawn' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, spawn_id, auth_headers, json }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n", .{}); + } else if (clone) |clone_id| { + const nm = name orelse ""; + const json = try std.fmt.allocPrint(allocator, "{{\"name\":\"{s}\"}}", .{nm}); + defer allocator.free(json); + const path = try std.fmt.allocPrint(allocator, "/images/{s}/clone", .{clone_id}); + defer allocator.free(path); + const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key); + defer allocator.free(auth_headers); + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/clone' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, clone_id, auth_headers, json }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n", .{}); + } else { + std.debug.print("{s}Error: No image action specified. Use --list, --info, --delete, --publish, etc.{s}\n", .{ RED, RESET }); + return 1; + } + return 0; + } + // Handle key command if (mem.eql(u8, args[1], "key")) { var extend = false;