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

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