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:
parent
aa1d14289e
commit
af995cdc1a
42 changed files with 9098 additions and 70 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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*)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))))))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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|_]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 '())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue