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
This commit is contained in:
russell@unturf.com 2026-01-17 10:25:20 -05:00
parent aa1d14289e
commit af995cdc1a
42 changed files with 9098 additions and 70 deletions

View file

@ -58,6 +58,8 @@ un [options] <source_file> # 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

View file

@ -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 <source_file>"
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()

View file

@ -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 <file>" >&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

View file

@ -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;

View file

@ -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] <source_file>")
(println " un.clj languages [--json]")
(println " un.clj session [options]")
(println " un.clj service [options]")
(println " un.clj service env <action> <service_id>")
(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*)

View file

@ -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.

View file

@ -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<string> 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] <source_file>" << 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<string> envs, files;
bool artifacts = false;

View file

@ -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] <source_file>\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [options]\n un.cr key [options]\n\nService env commands:\n env status <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [options]\n un.cr key [options]\n\nService env commands:\n env status <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> 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

View file

@ -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] <source_file>", 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 <action> <service_id> [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;

View file

@ -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<String?> getApiKeys(String? argsKey) {
@ -702,6 +717,118 @@ Future<void> cmdService(Args args) async {
exit(1);
}
Future<void> 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<void> 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 = <String, dynamic>{
'source_type': args.imageSourceType,
'source_id': args.imagePublish,
};
if (args.imageName != null) {
payload['name'] = args.imageName;
}
final result = await apiRequestCurl('/images/publish', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image published$reset');
print(jsonEncode(result));
return;
}
if (args.imageVisibility != null) {
if (args.imageVisibilityMode == null) {
stderr.writeln('${red}Error: --visibility requires MODE (private, unlisted, or public)$reset');
exit(1);
}
final payload = {'visibility': args.imageVisibilityMode};
await apiRequestCurl('/images/${args.imageVisibility}/visibility', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image visibility set to ${args.imageVisibilityMode}$reset');
return;
}
if (args.imageSpawn != null) {
final payload = <String, dynamic>{};
if (args.imageName != null) {
payload['name'] = args.imageName;
}
if (args.imagePorts != null) {
payload['ports'] = args.imagePorts!.split(',').map((p) => int.parse(p.trim())).toList();
}
final result = await apiRequestCurl('/images/${args.imageSpawn}/spawn', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Service spawned from image$reset');
print(jsonEncode(result));
return;
}
if (args.imageClone != null) {
final payload = <String, dynamic>{};
if (args.imageName != null) {
payload['name'] = args.imageName;
}
final result = await apiRequestCurl('/images/${args.imageClone}/clone', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Image cloned$reset');
print(jsonEncode(result));
return;
}
stderr.writeln('${red}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID$reset');
exit(1);
}
Future<void> cmdKey(Args args) async {
final keys = getApiKeys(args.apiKey);
final publicKey = keys[0]!;
@ -771,9 +898,20 @@ Args parseArgs(List<String> 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<String> 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<String> 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<String> 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<String> 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] <source_file>
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<String> 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 {

View file

@ -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 <action> <service_id>")
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()

View file

@ -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]).

View file

@ -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 <source_file>" 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
;

View file

@ -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] <source_file>'
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 <id> Export vault'
write(*, '(A)') ' service env delete <id> 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

View file

@ -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 <action> <target>
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 <action> <service_id> [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"
[<EntryPoint>]
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

View file

@ -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

View file

@ -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<String> 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] <source_file>
groovy un.groovy session [options]
groovy un.groovy service [options]
groovy un.groovy service env <action> <service_id> [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) {

View file

@ -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 <action> <id> 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

View file

@ -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<String, Object> publishImage(
String sourceType,
String sourceId,
String name,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> 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<String> 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<Map<String, Object>> images = listImages(null, publicKey, secretKey);
printImageList(images);
} else if (infoId != null) {
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<Map<String, Object>> images) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "VISIBILITY", "CREATED");
for (Map<String, Object> 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<String, Object> result = validateKeys(publicKey, secretKey);
printMap(result);
}
private static void handleLanguages(List<String> 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<String> 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<Map<String, Object>> sessions) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> session : sessions) {

View file

@ -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 <lang> 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);
}

View file

@ -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()

View file

@ -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<String>) {
@ -115,7 +129,9 @@ fun main(args: Array<String>) {
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<String> ?: 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<String, Any>(
"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<String, Any>()
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<String, Any>()
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<String>): 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<String>): 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<String>): 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] <source_file>
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())
}

View file

@ -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] <source_file>~%")
(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)))))))

View file

@ -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] <source_file>")
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

View file

@ -564,6 +564,149 @@ proc cmdKey(extend: bool, publicKey: string, secretKey: string) =
if errEnd > errStart:
echo "Error: " & response[errStart..<errEnd]
proc cmdLanguages(jsonOutput: bool, publicKey: string, secretKey: string) =
let authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/languages' {authHeaders}"""
let response = execCurl(cmd)
if jsonOutput:
# Extract languages array and output as JSON
let langStart = response.find("\"languages\":")
if langStart >= 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..<bracketEnd]
else:
echo "[]"
else:
echo "[]"
else:
echo "[]"
else:
# Extract each language and print one per line
let langStart = response.find("\"languages\":")
if langStart >= 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..<endPos]
pos = endPos + 1
else:
inc pos
proc cmdImage(list: bool, infoId, deleteId, lockId, unlockId, publishId, sourceType, visibilityId, visibilityMode, spawnId, cloneId, name, ports, publicKey, secretKey: string) =
if list:
let authHeaders = buildAuthHeaders("GET", "/images", "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/images' {authHeaders}"""
echo execCurl(cmd)
return
if infoId != "":
let path = fmt"/images/{infoId}"
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/images/{infoId}' {authHeaders}"""
echo execCurl(cmd)
return
if deleteId != "":
let path = fmt"/images/{deleteId}"
let authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/images/{deleteId}' {authHeaders}"""
discard execCurl(cmd)
echo GREEN & "Image deleted: " & deleteId & RESET
return
if lockId != "":
let path = fmt"/images/{lockId}/lock"
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{lockId}/lock' {authHeaders}"""
discard execCurl(cmd)
echo GREEN & "Image locked: " & lockId & RESET
return
if unlockId != "":
let path = fmt"/images/{unlockId}/unlock"
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{unlockId}/unlock' {authHeaders}"""
discard execCurl(cmd)
echo GREEN & "Image unlocked: " & unlockId & RESET
return
if publishId != "":
if sourceType == "":
stderr.writeLine(RED & "Error: --publish requires --source-type (service or snapshot)" & RESET)
quit(1)
var json = fmt"""{{"source_type":"{sourceType}","source_id":"{publishId}"""""
if name != "": json.add(fmt""","name":"{name}"""")
json.add("}")
let authHeaders = buildAuthHeaders("POST", "/images/publish", json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/publish' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
let response = execCurl(cmd)
echo GREEN & "Image published" & RESET
echo response
return
if visibilityId != "" and visibilityMode != "":
let json = fmt"""{{"visibility":"{visibilityMode}"}}"""
let path = fmt"/images/{visibilityId}/visibility"
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{visibilityId}/visibility' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
discard execCurl(cmd)
echo GREEN & "Image visibility set to " & visibilityMode & ": " & visibilityId & RESET
return
if spawnId != "":
var json = "{"
var hasContent = false
if name != "":
json.add(fmt""""name":"{name}"""")
hasContent = true
if ports != "":
if hasContent: json.add(",")
json.add(fmt""""ports":[{ports}]""")
json.add("}")
let path = fmt"/images/{spawnId}/spawn"
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{spawnId}/spawn' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
let response = execCurl(cmd)
echo GREEN & "Service spawned from image" & RESET
echo response
return
if cloneId != "":
var json = "{"
if name != "":
json.add(fmt""""name":"{name}"""")
json.add("}")
let path = fmt"/images/{cloneId}/clone"
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{cloneId}/clone' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
let response = execCurl(cmd)
echo GREEN & "Image cloned" & RESET
echo response
return
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone" & RESET)
quit(1)
proc main() =
var publicKey = getEnv("UNSANDBOX_PUBLIC_KEY", "")
var secretKey = getEnv("UNSANDBOX_SECRET_KEY", "")
@ -578,9 +721,28 @@ proc main() =
stderr.writeLine("Usage: un.nim [options] <source_file>")
stderr.writeLine(" un.nim session [options]")
stderr.writeLine(" un.nim service [options]")
stderr.writeLine(" un.nim image [options]")
stderr.writeLine(" un.nim service env <action> <service_id> [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 <id> Show vault status")
stderr.writeLine(" env set <id> 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

View file

@ -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] <source_file>\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 {

View file

@ -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] <source_file>\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 <action> <service_id>\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

View file

@ -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] <source_file>
$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)

View file

@ -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

View file

@ -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] <source_file>
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 {

View file

@ -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] <source_file>\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|_]

View file

@ -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()

View file

@ -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 <action> <service_id> [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 <id> Show vault status\n", file = stderr())
cat(" env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n", file = stderr())
cat(" env export <id> Export vault contents\n", file = stderr())
cat(" env delete <id> 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)
}
}

View file

@ -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<languages>.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<concurrency> // '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<images>.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<id> // 'N/A', %img<name> // '-', %img<visibility> // 'N/A', %img<created_at> // '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<id> // 'N/A'}";
say "Name: {%result<name> // '-'}";
say "Visibility: {%result<visibility> // 'N/A'}";
say "Created: {%result<created_at> // '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> = $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<id> // '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> = $name if $name;
if $ports {
%payload<ports> = $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<id> // 'N/A'}";
return;
}
if $clone-id {
my %payload;
%payload<name> = $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<id> // '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] <source_file>";
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);
}

View file

@ -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

View file

@ -2288,6 +2288,24 @@ struct CliOptions {
clone_name: Option<String>,
clone_shell: Option<String>,
clone_ports: Option<String>,
// Image options
image_list: bool,
image_info: Option<String>,
image_delete: Option<String>,
image_lock: Option<String>,
image_unlock: Option<String>,
image_publish: Option<String>,
image_source_type: Option<String>,
image_visibility_id: Option<String>,
image_visibility_mode: Option<String>,
image_spawn: Option<String>,
image_clone: Option<String>,
image_name: Option<String>,
image_ports: Option<String>,
// 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<u16> = 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() {

View file

@ -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] <source_file>\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 '())

View file

@ -1176,6 +1176,7 @@ func printHelp() {
un service-env <action> <id> 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)

View file

@ -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 <action> <service_id> \[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
}

View file

@ -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<void>
}
}
async function cmdLanguages(args: Args): Promise<void> {
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<void> {
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<void> {
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<void> {
} 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] <source_file>
${process.argv[1]} session [options]
${process.argv[1]} service [options]
${process.argv[1]} image [options]
${process.argv[1]} key [options]
${process.argv[1]} languages [--json]
Execute options:
-e KEY=VALUE Environment variable (multiple allowed)
@ -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);
}

View file

@ -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 <action> <service_id> [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 <id> Check vault status')
eprintln(' service env set <id> Set vault (-e KEY=VAL or --env-file FILE)')
eprintln(' service env export <id> Export vault contents')
eprintln(' service env delete <id> 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

View file

@ -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 <action> <service_id> [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 <id> Check vault status\n", .{});
std.debug.print(" service env set <id> Set vault (-e KEY=VAL or --env-file FILE)\n", .{});
std.debug.print(" service env export <id> Export vault contents\n", .{});
std.debug.print(" service env delete <id> 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;