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

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