feat(cli): Add languages and image commands to all 42 SDKs

- Add `un languages [--json]` command to list available execution languages
- Add `un image` command with full image management:
  - --list, --info, --delete, --lock, --unlock
  - --publish, --visibility, --spawn, --clone
- Update CLI_SPEC.md with new command documentation
- All implementations follow consistent patterns
This commit is contained in:
russell@unturf.com 2026-01-17 10:25:20 -05:00
parent aa1d14289e
commit af995cdc1a
42 changed files with 9098 additions and 70 deletions

View file

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