Support -f on service create and redeploy across all SDKs

Add input_files support to service redeploy for all 12 SDK
implementations that have service commands. Files passed via -f are
read, base64-encoded, and sent as input_files in the JSON payload.
Service create also gains -f support where it was missing.

Updated: bash, cpp, csharp, dotnet, go, java, javascript, perl,
php, ruby, rust, typescript
This commit is contained in:
russell@unturf.com 2026-03-02 06:13:43 -05:00
parent ac50544ac3
commit 369f3aa39d
13 changed files with 790 additions and 46 deletions

View file

@ -424,6 +424,7 @@ service_create() {
local name="$1" local name="$1"
local ports="${2:-}" local ports="${2:-}"
local bootstrap="${3:-}" local bootstrap="${3:-}"
local input_files_json="${4:-}"
local body local body
body=$(jq -n --arg name "$name" '{name: $name}') body=$(jq -n --arg name "$name" '{name: $name}')
@ -434,6 +435,9 @@ service_create() {
if [ -n "$bootstrap" ]; then if [ -n "$bootstrap" ]; then
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}') body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
fi fi
if [ -n "$input_files_json" ]; then
body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}')
fi
api_request "POST" "/services" "$body" api_request "POST" "/services" "$body"
} }
@ -472,10 +476,14 @@ service_set_unfreeze_on_demand() {
service_redeploy() { service_redeploy() {
local service_id="$1" local service_id="$1"
local bootstrap="${2:-}" local bootstrap="${2:-}"
local input_files_json="${3:-}"
local body="{}" local body="{}"
if [ -n "$bootstrap" ]; then if [ -n "$bootstrap" ]; then
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}') body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
fi fi
if [ -n "$input_files_json" ]; then
body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}')
fi
api_request "POST" "/services/$service_id/redeploy" "$body" api_request "POST" "/services/$service_id/redeploy" "$body"
} }
@ -902,6 +910,9 @@ cmd_service() {
local target="" local target=""
local name="" local name=""
local ports="" local ports=""
local bootstrap=""
local bootstrap_file=""
local -a files=()
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
@ -913,13 +924,47 @@ cmd_service() {
--lock) action="lock"; target="$2"; shift ;; --lock) action="lock"; target="$2"; shift ;;
--unlock) action="unlock"; target="$2"; shift ;; --unlock) action="unlock"; target="$2"; shift ;;
--logs) action="logs"; target="$2"; shift ;; --logs) action="logs"; target="$2"; shift ;;
--redeploy) action="redeploy"; target="$2"; shift ;;
--name) name="$2"; shift ;; --name) name="$2"; shift ;;
--ports) ports="$2"; shift ;; --ports) ports="$2"; shift ;;
--bootstrap) bootstrap="$2"; shift ;;
--bootstrap-file) bootstrap_file="$2"; shift ;;
-f|--file) files+=("$2"); shift ;;
*) ;; *) ;;
esac esac
shift shift
done done
# Build input_files JSON from -f args
local input_files_json=""
if [ ${#files[@]} -gt 0 ]; then
input_files_json="["
local first=1
for fpath in "${files[@]}"; do
if [ ! -f "$fpath" ]; then
echo -e "${RED}Error: File not found: $fpath${RESET}" >&2
exit 1
fi
local encoded
encoded=$(base64 -w0 "$fpath" 2>/dev/null || base64 "$fpath" 2>/dev/null)
local fname
fname=$(basename "$fpath")
[ "$first" -eq 0 ] && input_files_json="$input_files_json,"
input_files_json="$input_files_json{\"filename\":$(echo "$fname" | jq -Rs .),\"content\":$(echo "$encoded" | jq -Rs .)}"
first=0
done
input_files_json="$input_files_json]"
fi
# Resolve bootstrap from file if provided
if [ -n "$bootstrap_file" ]; then
if [ ! -f "$bootstrap_file" ]; then
echo -e "${RED}Error: Bootstrap file not found: $bootstrap_file${RESET}" >&2
exit 1
fi
bootstrap=$(cat "$bootstrap_file")
fi
case "$action" in case "$action" in
list) list)
local result local result
@ -954,14 +999,19 @@ cmd_service() {
result=$(service_logs "$target") result=$(service_logs "$target")
echo "$result" | jq -r '.logs // empty' echo "$result" | jq -r '.logs // empty'
;; ;;
redeploy)
local result
result=$(service_redeploy "$target" "$bootstrap" "$input_files_json")
echo -e "${GREEN}Service redeployed: $target${RESET}"
;;
*) *)
if [ -n "$name" ]; then if [ -n "$name" ]; then
local result local result
result=$(service_create "$name" "$ports" "") result=$(service_create "$name" "$ports" "$bootstrap" "$input_files_json")
echo -e "${GREEN}Service created${RESET}" echo -e "${GREEN}Service created${RESET}"
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"' echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
else else
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--name NAME" >&2 echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--redeploy ID|--name NAME" >&2
exit 1 exit 1
fi fi
;; ;;
@ -1138,8 +1188,12 @@ Service options:
--lock ID Lock service --lock ID Lock service
--unlock ID Unlock service --unlock ID Unlock service
--logs ID Get service logs --logs ID Get service logs
--redeploy ID Re-run bootstrap (supports -f, --bootstrap)
--name NAME Create service with name --name NAME Create service with name
--ports PORTS Service ports (comma-separated) --ports PORTS Service ports (comma-separated)
--bootstrap CMD Bootstrap command
--bootstrap-file FILE Bootstrap from file
-f, --file FILE Add input file (can repeat)
Snapshot options: Snapshot options:
--list List all snapshots --list List all snapshots

View file

@ -705,9 +705,32 @@ string service_unlock(const string& service_id, const string& public_key, const
return exec_curl(cmd); return exec_curl(cmd);
} }
string service_redeploy(const string& service_id, const string& bootstrap, const string& public_key, const string& secret_key) { string service_redeploy(const string& service_id, const string& bootstrap, const vector<string>& input_files, const string& public_key, const string& secret_key) {
string path = "/services/" + service_id + "/redeploy"; string path = "/services/" + service_id + "/redeploy";
string body = bootstrap.empty() ? "{}" : "{\"bootstrap\":\"" + escape_json(bootstrap) + "\"}"; ostringstream json;
json << "{";
bool has_field = false;
if (!bootstrap.empty()) {
json << "\"bootstrap\":\"" << escape_json(bootstrap) << "\"";
has_field = true;
}
if (!input_files.empty()) {
if (has_field) json << ",";
json << "\"input_files\":[";
for (size_t i = 0; i < input_files.size(); i++) {
if (i > 0) json << ",";
ifstream file(input_files[i], ios::binary);
if (!file) continue;
ostringstream content;
content << file.rdbuf();
string b64 = base64_encode(content.str());
string filename = input_files[i].substr(input_files[i].find_last_of("/\\") + 1);
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
}
json << "]";
}
json << "}";
string body = json.str();
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key); string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + path + "' " string cmd = "curl -s -X POST '" + API_BASE + path + "' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
@ -1202,7 +1225,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
cout << exec_curl(cmd) << endl; cout << exec_curl(cmd) << endl;
} }
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) { void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& redeploy, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) {
// Handle service env subcommand // Handle service env subcommand
if (!env_action.empty()) { if (!env_action.empty()) {
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key); cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
@ -1328,6 +1351,62 @@ void cmd_service(const string& name, const string& ports, const string& type, co
return; return;
} }
if (!redeploy.empty()) {
// Bootstrap is optional for redeploy:
// - If provided via --bootstrap or --bootstrap-file, use it
// - If omitted, API will use the stored encrypted bootstrap
string bootstrap_to_use = bootstrap;
if (!bootstrap_file.empty()) {
struct stat st;
if (stat(bootstrap_file.c_str(), &st) == 0) {
bootstrap_to_use = read_file(bootstrap_file);
} else {
cerr << RED << "Error: Bootstrap file not found: " << bootstrap_file << RESET << endl;
exit(1);
}
}
cout << YELLOW << "Redeploying service " << redeploy << "..." << RESET << endl;
ostringstream json;
json << "{";
bool has_field = false;
if (!bootstrap_to_use.empty()) {
if (!bootstrap_file.empty()) {
json << "\"bootstrap_content\":\"" << escape_json(bootstrap_to_use) << "\"";
} else {
json << "\"bootstrap\":\"" << escape_json(bootstrap_to_use) << "\"";
}
has_field = true;
}
if (!files.empty()) {
if (has_field) json << ",";
json << "\"input_files\":[";
for (size_t i = 0; i < files.size(); i++) {
if (i > 0) json << ",";
ifstream file(files[i], ios::binary);
if (!file) {
cerr << RED << "Error: Input file not found: " << files[i] << RESET << endl;
exit(1);
}
ostringstream content;
content << file.rdbuf();
string b64 = base64_encode(content.str());
string filename = files[i].substr(files[i].find_last_of("/\\") + 1);
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
}
json << "]";
}
json << "}";
string path = "/services/" + redeploy + "/redeploy";
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() + "'";
string result = exec_curl(cmd);
cout << result << endl;
return;
}
if (!dump_bootstrap.empty()) { if (!dump_bootstrap.empty()) {
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl; cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}"; string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
@ -1778,7 +1857,7 @@ int main(int argc, char* argv[]) {
if (cmd_type == "service") { if (cmd_type == "service") {
string name, ports, type, bootstrap, bootstrap_file; string name, ports, type, bootstrap, bootstrap_file;
bool list = false; bool list = false;
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network; string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, redeploy;
int vcpu = 0; int vcpu = 0;
vector<string> files; vector<string> files;
vector<string> envs; vector<string> envs;
@ -1816,6 +1895,7 @@ int main(int argc, char* argv[]) {
else if (arg == "--resize" && i+1 < argc) resize = argv[++i]; else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
else if (arg == "--execute" && i+1 < argc) execute = argv[++i]; else if (arg == "--execute" && i+1 < argc) execute = argv[++i];
else if (arg == "--command" && i+1 < argc) command = argv[++i]; else if (arg == "--command" && i+1 < argc) command = argv[++i];
else if (arg == "--redeploy" && i+1 < argc) redeploy = argv[++i];
else if (arg == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = argv[++i]; else if (arg == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = argv[++i];
else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i]; else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i];
else if (arg == "-n" && i+1 < argc) network = argv[++i]; else if (arg == "-n" && i+1 < argc) network = argv[++i];
@ -1832,7 +1912,7 @@ int main(int argc, char* argv[]) {
else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
} }
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key); cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, redeploy, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key);
return 0; return 0;
} }

View file

@ -92,6 +92,10 @@ class Un
{ {
CmdKey(parsedArgs); CmdKey(parsedArgs);
} }
else if (parsedArgs.Command == "languages")
{
CmdLanguages(parsedArgs);
}
else if (parsedArgs.SourceFile != null) else if (parsedArgs.SourceFile != null)
{ {
CmdExecute(parsedArgs); CmdExecute(parsedArgs);
@ -438,6 +442,32 @@ class Un
return; return;
} }
if (args.ServiceRedeploy != null)
{
var payload = new Dictionary<string, object>();
if (args.ServiceBootstrap != null)
{
payload["bootstrap"] = args.ServiceBootstrap;
}
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", "POST", payload.Count > 0 ? payload : null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service redeployed: {args.ServiceRedeploy}{RESET}");
return;
}
if (args.ServiceExecute != null) if (args.ServiceExecute != null)
{ {
var payload = new Dictionary<string, object> var payload = new Dictionary<string, object>
@ -529,6 +559,20 @@ class Un
{ {
payload["unfreeze_on_demand"] = true; payload["unfreeze_on_demand"] = true;
} }
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey); var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null; string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
@ -1234,10 +1278,12 @@ class Un
public string ServiceShowFreezePage = null; public string ServiceShowFreezePage = null;
public bool ServiceShowFreezePageEnabled = true; public bool ServiceShowFreezePageEnabled = true;
public bool ServiceCreateUnfreezeOnDemand = false; public bool ServiceCreateUnfreezeOnDemand = false;
public string ServiceRedeploy = null;
public string EnvFile = null; public string EnvFile = null;
public string EnvAction = null; public string EnvAction = null;
public string EnvTarget = null; public string EnvTarget = null;
public bool KeyExtend = false; public bool KeyExtend = false;
public bool LanguagesJson = false;
} }
static Args ParseArgs(string[] args) static Args ParseArgs(string[] args)
@ -1249,6 +1295,7 @@ class Un
if (arg == "session") result.Command = "session"; if (arg == "session") result.Command = "session";
else if (arg == "service") result.Command = "service"; else if (arg == "service") result.Command = "service";
else if (arg == "key") result.Command = "key"; else if (arg == "key") result.Command = "key";
else if (arg == "languages") result.Command = "languages";
else if (arg == "env" && result.Command == "service") else if (arg == "env" && result.Command == "service")
{ {
// Parse: service env <action> <target> // Parse: service env <action> <target>
@ -1295,12 +1342,98 @@ class Un
else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i]; else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i];
else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true"; else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true";
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true; else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
else if (arg == "--redeploy") result.ServiceRedeploy = args[++i];
else if (arg == "--extend") result.KeyExtend = true; else if (arg == "--extend") result.KeyExtend = true;
else if (arg == "--json") result.LanguagesJson = true;
else if (!arg.StartsWith("-")) result.SourceFile = arg; else if (!arg.StartsWith("-")) result.SourceFile = arg;
} }
return result; return result;
} }
static string GetLanguagesCachePath()
{
string home = Environment.GetEnvironmentVariable("HOME")
?? Environment.GetEnvironmentVariable("USERPROFILE")
?? ".";
return Path.Combine(home, ".unsandbox", "languages.json");
}
static List<string> LoadLanguagesCache()
{
string cachePath = GetLanguagesCachePath();
if (!File.Exists(cachePath)) return null;
try
{
string content = File.ReadAllText(cachePath);
double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds();
double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now - mtime < 3600)
{
var data = ParseJson(content);
if (data.ContainsKey("languages") && data["languages"] is List<object> langs)
return langs.ConvertAll(x => x.ToString());
}
}
catch { }
return null;
}
static void SaveLanguagesCache(List<string> languages)
{
try
{
string cachePath = GetLanguagesCachePath();
string cacheDir = Path.GetDirectoryName(cachePath);
if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir);
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var sb = new StringBuilder();
sb.Append("{\"languages\":[");
for (int i = 0; i < languages.Count; i++)
{
if (i > 0) sb.Append(",");
sb.Append("\"").Append(languages[i]).Append("\"");
}
sb.Append("],\"timestamp\":").Append(timestamp).Append("}");
File.WriteAllText(cachePath, sb.ToString());
}
catch { }
}
static void CmdLanguages(Args args)
{
// Try cache first
var languages = LoadLanguagesCache();
if (languages == null)
{
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
var result = ApiRequest("/languages", "GET", null, publicKey, secretKey);
languages = new List<string>();
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
languages = langs.ConvertAll(x => x.ToString());
SaveLanguagesCache(languages);
}
if (args.LanguagesJson)
{
var sb = new StringBuilder("[");
for (int i = 0; i < languages.Count; i++)
{
if (i > 0) sb.Append(",");
sb.Append("\"").Append(languages[i]).Append("\"");
}
sb.Append("]");
Console.WriteLine(sb.ToString());
}
else
{
foreach (var lang in languages)
Console.WriteLine(lang);
}
}
static void PrintHelp() static void PrintHelp()
{ {
Console.WriteLine(@"Usage: Un [options] <source_file> Console.WriteLine(@"Usage: Un [options] <source_file>
@ -1308,6 +1441,7 @@ class Un
Un service [options] Un service [options]
Un service env <action> <service_id> [options] Un service env <action> <service_id> [options]
Un key [options] Un key [options]
Un languages [--json]
Execute options: Execute options:
-e KEY=VALUE Set environment variable -e KEY=VALUE Set environment variable
@ -1340,6 +1474,7 @@ Service options:
--show-freeze-page-enabled BOOL Enable/disable (default: true) --show-freeze-page-enabled BOOL Enable/disable (default: true)
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service --with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
--destroy ID Destroy service --destroy ID Destroy service
--redeploy ID Re-run bootstrap (with optional --bootstrap, -f)
--execute ID Execute command in service --execute ID Execute command in service
--command CMD Command to execute (with --execute) --command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script --dump-bootstrap ID Dump bootstrap script
@ -1354,7 +1489,10 @@ Service env commands:
env delete ID Delete vault env delete ID Delete vault
Key options: Key options:
--extend Open browser to extend expired key"); --extend Open browser to extend expired key
Languages options:
--json Output as JSON array");
} }
} }
@ -1486,15 +1624,76 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); } catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
} }
/// <summary>Get available programming languages</summary> private const int LANGUAGES_CACHE_TTL = 3600; // 1 hour
private static string GetLanguagesCachePath()
{
string home = Environment.GetEnvironmentVariable("HOME")
?? Environment.GetEnvironmentVariable("USERPROFILE")
?? ".";
return Path.Combine(home, ".unsandbox", "languages.json");
}
private static List<string> LoadLanguagesCache()
{
string cachePath = GetLanguagesCachePath();
if (!File.Exists(cachePath)) return null;
try
{
string content = File.ReadAllText(cachePath);
double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds();
double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now - mtime < LANGUAGES_CACHE_TTL)
{
var data = ParseJson(content);
if (data.ContainsKey("languages") && data["languages"] is List<object> langs)
return langs.ConvertAll(x => x.ToString());
}
}
catch { }
return null;
}
private static void SaveLanguagesCache(List<string> languages)
{
try
{
string cachePath = GetLanguagesCachePath();
string cacheDir = Path.GetDirectoryName(cachePath);
if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir);
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var sb = new StringBuilder();
sb.Append("{\"languages\":[");
for (int i = 0; i < languages.Count; i++)
{
if (i > 0) sb.Append(",");
sb.Append("\"").Append(languages[i]).Append("\"");
}
sb.Append("],\"timestamp\":").Append(timestamp).Append("}");
File.WriteAllText(cachePath, sb.ToString());
}
catch { }
}
/// <summary>Get available programming languages (cached for 1 hour)</summary>
public static List<string> GetLanguages(string publicKey = null, string secretKey = null) public static List<string> GetLanguages(string publicKey = null, string secretKey = null)
{ {
// Try cache first
var cached = LoadLanguagesCache();
if (cached != null) return cached;
var (pk, sk) = ResolveKeys(publicKey, secretKey); var (pk, sk) = ResolveKeys(publicKey, secretKey);
try try
{ {
var result = ApiCall("/languages", "GET", null, pk, sk); var result = ApiCall("/languages", "GET", null, pk, sk);
if (result.ContainsKey("languages") && result["languages"] is List<object> langs) if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
return langs.ConvertAll(x => x.ToString()); {
var languages = langs.ConvertAll(x => x.ToString());
SaveLanguagesCache(languages);
return languages;
}
return new List<string>(); return new List<string>();
} }
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); } catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
@ -1632,7 +1831,7 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return null; } catch (Exception ex) { _lastError = ex.Message; return null; }
} }
public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, string publicKey = null, string secretKey = null) public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
{ {
var (pk, sk) = ResolveKeys(publicKey, secretKey); var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = new Dictionary<string, object> { ["name"] = name }; var payload = new Dictionary<string, object> { ["name"] = name };
@ -1645,6 +1844,7 @@ public static class Unsandbox
if (domains != null) payload["domains"] = domains; if (domains != null) payload["domains"] = domains;
if (bootstrap != null) payload["bootstrap"] = bootstrap; if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (networkMode != null) payload["network"] = networkMode; if (networkMode != null) payload["network"] = networkMode;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
try try
{ {
var result = ApiCall("/services", "POST", payload, pk, sk); var result = ApiCall("/services", "POST", payload, pk, sk);
@ -1696,10 +1896,16 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return false; } catch (Exception ex) { _lastError = ex.Message; return false; }
} }
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, string publicKey = null, string secretKey = null) public static bool ServiceRedeploy(string serviceId, string bootstrap = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
{ {
var (pk, sk) = ResolveKeys(publicKey, secretKey); var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null; Dictionary<string, object> payload = null;
if (bootstrap != null || (inputFiles != null && inputFiles.Count > 0))
{
payload = new Dictionary<string, object>();
if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
}
try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; } try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; }
catch (Exception ex) { _lastError = ex.Message; return false; } catch (Exception ex) { _lastError = ex.Message; return false; }
} }

View file

@ -392,7 +392,22 @@ void CmdService(Args args)
if (args.ServiceRedeploy != null) if (args.ServiceRedeploy != null)
{ {
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, null, publicKey, secretKey); var payload = new Dictionary<string, object>();
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, payload.Count > 0 ? payload : null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}"); Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
return; return;
} }
@ -452,6 +467,20 @@ void CmdService(Args args)
if (args.Network != null) payload["network"] = args.Network; if (args.Network != null) payload["network"] = args.Network;
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu; if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true; if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey); var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null; var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
@ -1085,7 +1114,7 @@ Service options:
--lock ID Prevent deletion --lock ID Prevent deletion
--unlock ID Allow deletion --unlock ID Allow deletion
--resize ID Resize (use with -v) --resize ID Resize (use with -v)
--redeploy ID Re-run bootstrap --redeploy ID Re-run bootstrap (use -f to include input files)
--snapshot ID Create snapshot from service --snapshot ID Create snapshot from service
--unfreeze-on-demand ID Set unfreeze-on-demand for service --unfreeze-on-demand ID Set unfreeze-on-demand for service
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true) --unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
@ -1098,6 +1127,7 @@ Service options:
--dump-bootstrap ID Dump bootstrap script --dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap) --dump-file FILE File to save bootstrap (with --dump-bootstrap)
-e KEY=VALUE Set vault env var (with --name or env set) -e KEY=VALUE Set vault env var (with --name or env set)
-f FILE Add input file (with --name or --redeploy)
--env-file FILE Load vault vars from file --env-file FILE Load vault vars from file
Service env commands: Service env commands:
@ -1406,7 +1436,7 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return null; } catch (Exception ex) { _lastError = ex.Message; return null; }
} }
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, string? publicKey = null, string? secretKey = null) public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
{ {
var (pk, sk) = ResolveKeys(publicKey, secretKey); var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = new Dictionary<string, object> { ["name"] = name }; var payload = new Dictionary<string, object> { ["name"] = name };
@ -1414,6 +1444,7 @@ public static class Unsandbox
if (domains != null) payload["domains"] = domains; if (domains != null) payload["domains"] = domains;
if (bootstrap != null) payload["bootstrap"] = bootstrap; if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (networkMode != null) payload["network"] = networkMode; if (networkMode != null) payload["network"] = networkMode;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
try try
{ {
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk); var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
@ -1465,10 +1496,16 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return false; } catch (Exception ex) { _lastError = ex.Message; return false; }
} }
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, string? publicKey = null, string? secretKey = null) public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
{ {
var (pk, sk) = ResolveKeys(publicKey, secretKey); var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null; Dictionary<string, object>? payload = null;
if (bootstrap != null || inputFiles != null)
{
payload = new Dictionary<string, object>();
if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
}
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; } try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
catch (Exception ex) { _lastError = ex.Message; return false; } catch (Exception ex) { _lastError = ex.Message; return false; }
} }

View file

@ -21,6 +21,7 @@ import (
"bytes" "bytes"
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -840,12 +841,19 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int
// Service Operations // Service Operations
// ============================================================================ // ============================================================================
// InputFile represents a file to upload with a service create or redeploy.
type InputFile struct {
Filename string `json:"filename"`
Content string `json:"content"` // base64-encoded
}
// ServiceOptions contains optional parameters for service creation. // ServiceOptions contains optional parameters for service creation.
type ServiceOptions struct { type ServiceOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted" NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use for bootstrap Shell string // Shell to use for bootstrap
VCPU int // Number of virtual CPUs VCPU int // Number of virtual CPUs
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
InputFiles []InputFile // Files to include (written to /tmp/ in container)
} }
// ServiceUpdateOptions contains optional parameters for service updates. // ServiceUpdateOptions contains optional parameters for service updates.
@ -902,6 +910,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin
if opts.UnfreezeOnDemand { if opts.UnfreezeOnDemand {
data["unfreeze_on_demand"] = true data["unfreeze_on_demand"] = true
} }
if len(opts.InputFiles) > 0 {
data["input_files"] = opts.InputFiles
}
} }
return makeRequest("POST", "/services", creds, data) return makeRequest("POST", "/services", creds, data)
@ -1017,11 +1028,15 @@ func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interfac
// creds: API credentials // creds: API credentials
// serviceID: Service ID // serviceID: Service ID
// bootstrap: New bootstrap script (empty string to keep existing) // bootstrap: New bootstrap script (empty string to keep existing)
func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) { // inputFiles: Optional files to include (written to /tmp/ in container)
func RedeployService(creds *Credentials, serviceID string, bootstrap string, inputFiles []InputFile) (map[string]interface{}, error) {
data := make(map[string]interface{}) data := make(map[string]interface{})
if bootstrap != "" { if bootstrap != "" {
data["bootstrap"] = bootstrap data["bootstrap"] = bootstrap
} }
if len(inputFiles) > 0 {
data["input_files"] = inputFiles
}
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data) return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
} }
@ -1899,6 +1914,22 @@ func readFileContents(path string) (string, error) {
return string(data), nil return string(data), nil
} }
// buildInputFiles reads files from paths and returns InputFile structs with base64-encoded content.
func buildInputFiles(paths []string) ([]InputFile, error) {
var files []InputFile
for _, fpath := range paths {
data, err := os.ReadFile(fpath)
if err != nil {
return nil, fmt.Errorf("cannot read input file %s: %w", fpath, err)
}
files = append(files, InputFile{
Filename: filepath.Base(fpath),
Content: base64.StdEncoding.EncodeToString(data),
})
}
return files, nil
}
// readEnvFile reads environment variables from a .env file // readEnvFile reads environment variables from a .env file
func readEnvFile(path string) (map[string]string, error) { func readEnvFile(path string) (map[string]string, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@ -2353,7 +2384,15 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
// Redeploy service // Redeploy service
if fs.redeploy != "" { if fs.redeploy != "" {
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap) var inputFiles []InputFile
if len(opts.Files) > 0 {
var err error
inputFiles, err = buildInputFiles(opts.Files)
if err != nil {
return cliError(err.Error(), ExitGeneralError)
}
}
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap, inputFiles)
if err != nil { if err != nil {
return cliError(err.Error(), ExitAPIError) return cliError(err.Error(), ExitAPIError)
} }
@ -2412,6 +2451,13 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
if opts.VCPU > 0 { if opts.VCPU > 0 {
serviceOpts.VCPU = opts.VCPU serviceOpts.VCPU = opts.VCPU
} }
if len(opts.Files) > 0 {
inputFiles, err := buildInputFiles(opts.Files)
if err != nil {
return cliError(err.Error(), ExitGeneralError)
}
serviceOpts.InputFiles = inputFiles
}
service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts) service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
if err != nil { if err != nil {

View file

@ -1493,6 +1493,31 @@ public class Un {
String bootstrap, String bootstrap,
String publicKey, String publicKey,
String secretKey String secretKey
) throws IOException {
return createService(name, ports, bootstrap, null, publicKey, secretKey);
}
/**
* Create a new service (long-running container) with optional input files.
*
* @param name Service name
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_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> createService(
String name,
String ports,
String bootstrap,
List<Map<String, String>> inputFiles,
String publicKey,
String secretKey
) throws IOException { ) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey); String[] creds = resolveCredentials(publicKey, secretKey);
@ -1517,6 +1542,9 @@ public class Un {
data.put("bootstrap", bootstrap); data.put("bootstrap", bootstrap);
} }
} }
if (inputFiles != null && !inputFiles.isEmpty()) {
data.put("input_files", inputFiles);
}
return makeRequest("POST", "/services", creds[0], creds[1], data); return makeRequest("POST", "/services", creds[0], creds[1], data);
} }
@ -1542,6 +1570,33 @@ public class Un {
boolean unfreezeOnDemand, boolean unfreezeOnDemand,
String publicKey, String publicKey,
String secretKey String secretKey
) throws IOException {
return createService(name, ports, bootstrap, unfreezeOnDemand, null, publicKey, secretKey);
}
/**
* Create a new service (long-running container) with unfreeze-on-demand option and input files.
*
* @param name Service name (used for hostname)
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param unfreezeOnDemand If true, frozen service will auto-wake on HTTP request
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_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> createService(
String name,
String ports,
String bootstrap,
boolean unfreezeOnDemand,
List<Map<String, String>> inputFiles,
String publicKey,
String secretKey
) throws IOException { ) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey); String[] creds = resolveCredentials(publicKey, secretKey);
@ -1569,6 +1624,9 @@ public class Un {
if (unfreezeOnDemand) { if (unfreezeOnDemand) {
data.put("unfreeze_on_demand", true); data.put("unfreeze_on_demand", true);
} }
if (inputFiles != null && !inputFiles.isEmpty()) {
data.put("input_files", inputFiles);
}
return makeRequest("POST", "/services", creds[0], creds[1], data); return makeRequest("POST", "/services", creds[0], creds[1], data);
} }
@ -1901,9 +1959,34 @@ public class Un {
String serviceId, String serviceId,
String publicKey, String publicKey,
String secretKey String secretKey
) throws IOException {
return redeployService(serviceId, null, publicKey, secretKey);
}
/**
* Redeploy a service (re-run bootstrap script) with optional input files.
*
* @param serviceId Service ID to redeploy
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with redeploy confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> redeployService(
String serviceId,
List<Map<String, String>> inputFiles,
String publicKey,
String secretKey
) throws IOException { ) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey); String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>()); Map<String, Object> data = new LinkedHashMap<>();
if (inputFiles != null && !inputFiles.isEmpty()) {
data.put("input_files", inputFiles);
}
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], data);
} }
/** /**
@ -2859,7 +2942,7 @@ public class Un {
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language); handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
break; break;
case "service": case "service":
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars); handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars, files);
break; break;
case "snapshot": case "snapshot":
handleSnapshot(positionalArgs, publicKey, secretKey); handleSnapshot(positionalArgs, publicKey, secretKey);
@ -3181,7 +3264,8 @@ public class Un {
String secretKey, String secretKey,
String networkMode, String networkMode,
int vcpu, int vcpu,
List<String> envVars List<String> envVars,
List<String> files
) throws Exception { ) throws Exception {
// Check for "env" subcommand // Check for "env" subcommand
if (args.size() > 1 && args.get(1).equals("env")) { if (args.size() > 1 && args.get(1).equals("env")) {
@ -3347,14 +3431,17 @@ public class Un {
System.err.print(stderr); System.err.print(stderr);
} }
} else if (redeployId != null) { } else if (redeployId != null) {
redeployService(redeployId, publicKey, secretKey); // Build input_files from -f args
List<Map<String, String>> inputFiles = buildInputFiles(files);
redeployService(redeployId, inputFiles, publicKey, secretKey);
System.out.println("Service redeployed: " + redeployId); System.out.println("Service redeployed: " + redeployId);
} else if (snapshotId != null) { } else if (snapshotId != null) {
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null); String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
System.out.println("Snapshot created: " + snapId); System.out.println("Snapshot created: " + snapId);
} else if (name != null) { } else if (name != null) {
// Create new service // Build input_files from -f args
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey); List<Map<String, String>> inputFiles = buildInputFiles(files);
Map<String, Object> result = createService(name, ports, bootstrap, inputFiles, publicKey, secretKey);
System.out.println("Service created:"); System.out.println("Service created:");
printMap(result); printMap(result);
} else { } else {
@ -3363,6 +3450,26 @@ public class Un {
} }
} }
/**
* Build input_files list from -f file paths: read each file, base64-encode, return list of maps.
*/
private static List<Map<String, String>> buildInputFiles(List<String> filePaths) throws IOException {
if (filePaths == null || filePaths.isEmpty()) {
return null;
}
List<Map<String, String>> inputFiles = new ArrayList<>();
for (String fpath : filePaths) {
Path p = Paths.get(fpath);
byte[] content = Files.readAllBytes(p);
String encoded = Base64.getEncoder().encodeToString(content);
Map<String, String> entry = new LinkedHashMap<>();
entry.put("filename", p.getFileName().toString());
entry.put("content", encoded);
inputFiles.add(entry);
}
return inputFiles;
}
private static void handleServiceEnv( private static void handleServiceEnv(
List<String> args, List<String> args,
String publicKey, String publicKey,

View file

@ -1093,6 +1093,7 @@ async function listServices(publicKey, secretKey) {
* - domains: Array of custom domains * - domains: Array of custom domains
* - serviceType: Service type for SRV records (minecraft, mumble, etc.) * - serviceType: Service type for SRV records (minecraft, mumble, etc.)
* - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic * - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic
* - inputFiles: Array of {filename, content} objects (content is base64-encoded)
* *
* Returns: Promise<Object> (service info with service_id) * Returns: Promise<Object> (service info with service_id)
*/ */
@ -1114,6 +1115,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre
if (opts.domains) data.custom_domains = opts.domains; if (opts.domains) data.custom_domains = opts.domains;
if (opts.serviceType) data.service_type = opts.serviceType; if (opts.serviceType) data.service_type = opts.serviceType;
if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true; if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true;
if (opts.inputFiles && opts.inputFiles.length > 0) data.input_files = opts.inputFiles;
return makeRequest('POST', '/services', publicKey, secretKey, data); return makeRequest('POST', '/services', publicKey, secretKey, data);
} }
@ -1331,10 +1333,11 @@ async function exportServiceEnv(serviceId, publicKey, secretKey) {
* Args: * Args:
* serviceId: Service ID to redeploy * serviceId: Service ID to redeploy
* bootstrap: Optional new bootstrap script content or URL * bootstrap: Optional new bootstrap script content or URL
* inputFiles: Optional array of {filename, content} objects (content is base64-encoded)
* *
* Returns: Promise<Object> (redeploy confirmation) * Returns: Promise<Object> (redeploy confirmation)
*/ */
async function redeployService(serviceId, bootstrap = null, publicKey, secretKey) { async function redeployService(serviceId, bootstrap = null, inputFiles = null, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey); [publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {}; const data = {};
if (bootstrap) { if (bootstrap) {
@ -1344,6 +1347,7 @@ async function redeployService(serviceId, bootstrap = null, publicKey, secretKey
data.bootstrap_content = bootstrap; data.bootstrap_content = bootstrap;
} }
} }
if (inputFiles && inputFiles.length > 0) data.input_files = inputFiles;
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data); return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
} }
@ -2146,7 +2150,7 @@ SERVICE COMMANDS:
node un.js service --lock <id> Prevent deletion node un.js service --lock <id> Prevent deletion
node un.js service --unlock <id> Allow deletion node un.js service --unlock <id> Allow deletion
node un.js service --execute <id> <cmd> Run command in service node un.js service --execute <id> <cmd> Run command in service
node un.js service --redeploy <id> Re-run bootstrap node un.js service --redeploy <id> Re-run bootstrap (supports -f)
node un.js service --snapshot <id> Create snapshot node un.js service --snapshot <id> Create snapshot
SERVICE ENV COMMANDS: SERVICE ENV COMMANDS:
@ -2732,7 +2736,19 @@ async function handleService(opts) {
if (opts.bootstrapFile) { if (opts.bootstrapFile) {
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8'); bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
} }
await redeployService(opts.redeploy, bootstrap, pk, sk); // Build input_files from -f args
let inputFiles = null;
if (opts.files && opts.files.length > 0) {
inputFiles = [];
for (const fpath of opts.files) {
const content = fs.readFileSync(fpath);
inputFiles.push({
filename: path.basename(fpath),
content: content.toString('base64'),
});
}
}
await redeployService(opts.redeploy, bootstrap, inputFiles, pk, sk);
console.log(`Service ${opts.redeploy} redeployed.`); console.log(`Service ${opts.redeploy} redeployed.`);
return; return;
} }
@ -2786,6 +2802,17 @@ async function handleService(opts) {
if (opts.type) { if (opts.type) {
serviceOpts.serviceType = opts.type; serviceOpts.serviceType = opts.type;
} }
// Build input_files from -f args
if (opts.files && opts.files.length > 0) {
serviceOpts.inputFiles = [];
for (const fpath of opts.files) {
const content = fs.readFileSync(fpath);
serviceOpts.inputFiles.push({
filename: path.basename(fpath),
content: content.toString('base64'),
});
}
}
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk); const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
console.log(`Service created: ${service.service_id}`); console.log(`Service created: ${service.service_id}`);

View file

@ -472,6 +472,7 @@ sub service_redeploy {
my ($service_id, %opts) = @_; my ($service_id, %opts) = @_;
my $body = {}; my $body = {};
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap}; $body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
$body->{input_files} = $opts{input_files} if $opts{input_files};
return api_request('POST', "/services/$service_id/redeploy", $body, %opts); return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
} }
@ -998,7 +999,12 @@ sub cmd_service {
} }
if ($options->{redeploy}) { if ($options->{redeploy}) {
Un::service_redeploy($options->{redeploy}, bootstrap => $options->{bootstrap}); my %opts;
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
if ($options->{files} && @{$options->{files}}) {
$opts{input_files} = build_input_files(@{$options->{files}});
}
Un::service_redeploy($options->{redeploy}, %opts);
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n"; print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
return; return;
} }

View file

@ -1339,13 +1339,18 @@ class Unsandbox {
* @param string $serviceId Service ID * @param string $serviceId Service ID
* @param string|null $publicKey Optional API key * @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret * @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'input_files'
* @return array Response array with redeploy confirmation * @return array Response array with redeploy confirmation
* @throws CredentialsException Missing credentials * @throws CredentialsException Missing credentials
* @throws ApiException API request failed * @throws ApiException API request failed
*/ */
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array { public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null, array $opts = []): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey); [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []); $data = [];
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, $data);
} }
/** /**
@ -2603,7 +2608,31 @@ class Unsandbox {
} }
if ($serviceOpts['redeploy']) { if ($serviceOpts['redeploy']) {
$result = $this->redeployService($serviceOpts['redeploy']); $redeployOpts = [];
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
if (!empty($inputFiles)) {
$redeployOpts['input_files'] = $inputFiles;
}
$result = $this->redeployService($serviceOpts['redeploy'], null, null, $redeployOpts);
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n"; echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
return; return;
} }
@ -2647,6 +2676,31 @@ class Unsandbox {
$createOpts['unfreeze_on_demand'] = true; $createOpts['unfreeze_on_demand'] = true;
} }
// Handle input files
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
if (!empty($inputFiles)) {
$createOpts['input_files'] = $inputFiles;
}
// Handle bootstrap from file // Handle bootstrap from file
$bootstrap = $serviceOpts['bootstrap'] ?? ''; $bootstrap = $serviceOpts['bootstrap'] ?? '';
if (!empty($serviceOpts['bootstrap_file'])) { if (!empty($serviceOpts['bootstrap_file'])) {
@ -3438,7 +3492,7 @@ SERVICE OPTIONS:
--lock ID Prevent service deletion --lock ID Prevent service deletion
--unlock ID Allow service deletion --unlock ID Allow service deletion
--resize ID Resize service (with --vcpu) --resize ID Resize service (with --vcpu)
--redeploy ID Re-run bootstrap --redeploy ID Re-run bootstrap (supports -f/-F for input files)
--execute ID 'cmd' Execute command in service --execute ID 'cmd' Execute command in service
--snapshot ID Create service snapshot --snapshot ID Create service snapshot

View file

@ -57,6 +57,7 @@ require 'openssl'
require 'fileutils' require 'fileutils'
require 'optparse' require 'optparse'
require 'cgi' require 'cgi'
require 'base64'
# Unsandbox Ruby SDK module (synchronous) # Unsandbox Ruby SDK module (synchronous)
module Un module Un
@ -922,6 +923,7 @@ module Un
# @param custom_domains [Array<String>, nil] Custom domains for the service # @param custom_domains [Array<String>, nil] Custom domains for the service
# @param service_type [String, nil] Service type for SRV records (e.g., "minecraft") # @param service_type [String, nil] Service type for SRV records (e.g., "minecraft")
# @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false) # @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false)
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
# @return [Hash] Response hash with service_id # @return [Hash] Response hash with service_id
# @raise [CredentialsError] If no credentials found # @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails # @raise [APIError] If API request fails
@ -929,7 +931,7 @@ module Un
# @example # @example
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx") # result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
# puts result["service_id"] # puts result["service_id"]
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false) def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false, input_files: nil)
pk, sk = resolve_credentials(public_key, secret_key) pk, sk = resolve_credentials(public_key, secret_key)
data = { data = {
name: name, name: name,
@ -941,6 +943,7 @@ module Un
data[:custom_domains] = custom_domains if custom_domains data[:custom_domains] = custom_domains if custom_domains
data[:service_type] = service_type if service_type data[:service_type] = service_type if service_type
data[:unfreeze_on_demand] = unfreeze_on_demand if unfreeze_on_demand data[:unfreeze_on_demand] = unfreeze_on_demand if unfreeze_on_demand
data[:input_files] = input_files if input_files && !input_files.empty?
make_request('POST', '/services', pk, sk, data) make_request('POST', '/services', pk, sk, data)
end end
@ -1202,16 +1205,18 @@ module Un
# @param public_key [String, nil] Optional API key # @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret # @param secret_key [String, nil] Optional API secret
# @param bootstrap [String, nil] New bootstrap script (optional) # @param bootstrap [String, nil] New bootstrap script (optional)
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
# @return [Hash] Response hash with redeploy confirmation # @return [Hash] Response hash with redeploy confirmation
# @raise [CredentialsError] If no credentials found # @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails # @raise [APIError] If API request fails
# #
# @example # @example
# Un.redeploy_service(service_id) # Un.redeploy_service(service_id)
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil) def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil, input_files: nil)
pk, sk = resolve_credentials(public_key, secret_key) pk, sk = resolve_credentials(public_key, secret_key)
data = {} data = {}
data[:bootstrap] = bootstrap if bootstrap data[:bootstrap] = bootstrap if bootstrap
data[:input_files] = input_files if input_files && !input_files.empty?
make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data) make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data)
end end
@ -2420,7 +2425,17 @@ module Un
elsif service_opts[:bootstrap] elsif service_opts[:bootstrap]
bootstrap = service_opts[:bootstrap] bootstrap = service_opts[:bootstrap]
end end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, **creds) # Build input_files from -f args
service_input_files = nil
unless options[:files].empty?
service_input_files = options[:files].map do |fpath|
{
filename: File.basename(fpath),
content: Base64.strict_encode64(File.binread(fpath))
}
end
end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, input_files: service_input_files, **creds)
puts "Service #{service_opts[:redeploy]} redeployed" puts "Service #{service_opts[:redeploy]} redeployed"
elsif service_opts[:execute] elsif service_opts[:execute]
cmd = service_opts[:execute_cmd] cmd = service_opts[:execute_cmd]
@ -2453,6 +2468,17 @@ module Un
exit(EXIT_INVALID_ARGS) exit(EXIT_INVALID_ARGS)
end end
# Build input_files from -f args
service_input_files = nil
unless options[:files].empty?
service_input_files = options[:files].map do |fpath|
{
filename: File.basename(fpath),
content: Base64.strict_encode64(File.binread(fpath))
}
end
end
result = create_service( result = create_service(
service_opts[:name], service_opts[:name],
service_opts[:ports], service_opts[:ports],
@ -2461,6 +2487,7 @@ module Un
vcpu: options[:vcpu], vcpu: options[:vcpu],
custom_domains: service_opts[:domains], custom_domains: service_opts[:domains],
service_type: service_opts[:type], service_type: service_opts[:type],
input_files: service_input_files,
**creds **creds
) )
puts "Service created: #{result['service_id']}" puts "Service created: #{result['service_id']}"

View file

@ -32,6 +32,9 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
# Base64 encoding (for input_files)
base64 = "0.22"
# HMAC-SHA256 authentication # HMAC-SHA256 authentication
hmac = "0.12" hmac = "0.12"
sha2 = "0.10" sha2 = "0.10"

View file

@ -58,6 +58,7 @@
// - TTL: 1 hour // - TTL: 1 hour
// - Updated on successful API calls // - Updated on successful API calls
use base64::Engine;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -382,6 +383,17 @@ pub struct ServiceCreateOptions {
pub bootstrap_url: Option<String>, pub bootstrap_url: Option<String>,
/// Whether to enable automatic unfreezing on incoming HTTP requests /// Whether to enable automatic unfreezing on incoming HTTP requests
pub unfreeze_on_demand: Option<bool>, pub unfreeze_on_demand: Option<bool>,
/// Input files to upload (filename + base64-encoded content)
pub input_files: Option<Vec<InputFile>>,
}
/// A file to upload with a service create or redeploy request
#[derive(Debug, Clone, Serialize)]
pub struct InputFile {
/// Filename (basename only)
pub filename: String,
/// Base64-encoded file content
pub content: String,
} }
/// Options for updating a service /// Options for updating a service
@ -1997,6 +2009,11 @@ pub fn create_service(
if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand { if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand {
body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand); body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand);
} }
if let Some(input_files) = opts.input_files {
if !input_files.is_empty() {
body["input_files"] = serde_json::json!(input_files);
}
}
} }
make_request("POST", "/services", creds, Some(&body)) make_request("POST", "/services", creds, Some(&body))
@ -2269,12 +2286,22 @@ pub fn export_service_env(service_id: &str, creds: &Credentials) -> Result<Strin
/// # Arguments /// # Arguments
/// * `service_id` - Service ID to redeploy /// * `service_id` - Service ID to redeploy
/// * `creds` - API credentials /// * `creds` - API credentials
/// * `input_files` - Optional files to upload (filename + base64-encoded content)
/// ///
/// # Returns /// # Returns
/// Updated Service information /// Updated Service information
pub fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service> { pub fn redeploy_service(
service_id: &str,
creds: &Credentials,
input_files: Option<Vec<InputFile>>,
) -> Result<Service> {
let path = format!("/services/{}/redeploy", service_id); let path = format!("/services/{}/redeploy", service_id);
let body = serde_json::json!({}); let mut body = serde_json::json!({});
if let Some(files) = input_files {
if !files.is_empty() {
body["input_files"] = serde_json::json!(files);
}
}
make_request("POST", &path, creds, Some(&body)) make_request("POST", &path, creds, Some(&body))
} }
@ -2861,7 +2888,7 @@ SESSION COMMANDS:
SERVICE COMMANDS: SERVICE COMMANDS:
un service --list List all services un service --list List all services
un service --name NAME --ports P Create service un service --name NAME --ports P [-f FILE] Create service
un service --info ID Get service details un service --info ID Get service details
un service --logs ID Get all logs un service --logs ID Get all logs
un service --tail ID Get last 9000 lines un service --tail ID Get last 9000 lines
@ -2871,7 +2898,7 @@ SERVICE COMMANDS:
un service --lock ID Prevent deletion un service --lock ID Prevent deletion
un service --unlock ID Allow deletion un service --unlock ID Allow deletion
un service --execute ID 'cmd' Run command un service --execute ID 'cmd' Run command
un service --redeploy ID Re-run bootstrap un service --redeploy ID [-f FILE] Re-run bootstrap
un service --snapshot ID Create snapshot un service --snapshot ID Create snapshot
un service env status ID Show vault status un service env status ID Show vault status
un service env set ID Set env vars un service env set ID Set env vars
@ -3586,6 +3613,33 @@ fn cmd_session(opts: &CliOptions) -> i32 {
} }
} }
/// Build input_files from -f file paths: read each file, base64 encode, return as InputFile vec.
fn build_input_files(files: &[String]) -> Option<Vec<InputFile>> {
if files.is_empty() {
return None;
}
let engine = base64::engine::general_purpose::STANDARD;
let mut input_files = Vec::new();
for fpath in files {
let path = std::path::Path::new(fpath);
match fs::read(path) {
Ok(data) => {
let filename = path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| fpath.clone());
input_files.push(InputFile {
filename,
content: engine.encode(&data),
});
}
Err(e) => {
eprintln!("Warning: Cannot read file '{}': {}", fpath, e);
}
}
}
if input_files.is_empty() { None } else { Some(input_files) }
}
fn cmd_service(opts: &CliOptions) -> i32 { fn cmd_service(opts: &CliOptions) -> i32 {
let creds = match get_credentials(opts) { let creds = match get_credentials(opts) {
Ok(c) => c, Ok(c) => c,
@ -3736,7 +3790,9 @@ fn cmd_service(opts: &CliOptions) -> i32 {
} }
if let Some(ref id) = opts.service_redeploy { if let Some(ref id) = opts.service_redeploy {
match redeploy_service(id, &creds) { // Build input_files from -f args
let service_input_files = build_input_files(&opts.files);
match redeploy_service(id, &creds, service_input_files) {
Ok(service) => { Ok(service) => {
println!("Service {} redeployed.", service.service_id); println!("Service {} redeployed.", service.service_id);
return EXIT_SUCCESS; return EXIT_SUCCESS;
@ -3809,12 +3865,16 @@ fn cmd_service(opts: &CliOptions) -> i32 {
String::new() String::new()
}; };
// Build input_files from -f args
let service_input_files = build_input_files(&opts.files);
let service_opts = ServiceCreateOptions { let service_opts = ServiceCreateOptions {
network_mode: opts.network.clone(), network_mode: opts.network.clone(),
vcpu: opts.vcpu, vcpu: opts.vcpu,
domains: opts.service_domains.as_ref().map(|d| { domains: opts.service_domains.as_ref().map(|d| {
d.split(',').map(|s| s.trim().to_string()).collect() d.split(',').map(|s| s.trim().to_string()).collect()
}), }),
input_files: service_input_files,
..Default::default() ..Default::default()
}; };

View file

@ -195,6 +195,7 @@ interface Args {
setUnfreezeOnDemand: string | null; setUnfreezeOnDemand: string | null;
showFreezePage: boolean | null; showFreezePage: boolean | null;
setShowFreezePage: string | null; setShowFreezePage: string | null;
redeploy: string | null;
} }
interface ApiKeys { interface ApiKeys {
@ -897,6 +898,37 @@ async function cmdService(args: Args): Promise<void> {
return; return;
} }
if (args.redeploy) {
const payload: any = {};
if (args.bootstrap) {
payload.bootstrap = args.bootstrap;
}
if (args.bootstrapFile) {
if (!fs.existsSync(args.bootstrapFile)) {
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
process.exit(1);
}
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
}
if (args.files && args.files.length > 0) {
payload.input_files = args.files.map(filepath => {
try {
const content = fs.readFileSync(filepath);
return {
filename: path.basename(filepath),
content_base64: content.toString('base64')
};
} catch (e) {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
process.exit(1);
}
});
}
await apiRequest(`/services/${args.redeploy}/redeploy`, "POST", payload, keys);
console.log(`${GREEN}Service redeployed: ${args.redeploy}${RESET}`);
return;
}
if (args.execute) { if (args.execute) {
const payload = { command: args.command_arg }; const payload = { command: args.command_arg };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys); const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
@ -1225,6 +1257,7 @@ function parseArgs(argv: string[]): Args {
setUnfreezeOnDemand: null, setUnfreezeOnDemand: null,
showFreezePage: null, showFreezePage: null,
setShowFreezePage: null, setShowFreezePage: null,
redeploy: null,
}; };
let i = 2; let i = 2;
@ -1397,6 +1430,9 @@ function parseArgs(argv: string[]): Args {
} else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) { } else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) {
args.setShowFreezePage = argv[++i]; args.setShowFreezePage = argv[++i];
i++; i++;
} else if (arg === '--redeploy' && i + 1 < argv.length) {
args.redeploy = argv[++i];
i++;
} else if (!arg.startsWith('-')) { } else if (!arg.startsWith('-')) {
args.sourceFile = arg; args.sourceFile = arg;
i++; i++;
@ -1474,6 +1510,7 @@ Service options:
--unfreeze ID Unfreeze service --unfreeze ID Unfreeze service
--destroy ID Destroy service --destroy ID Destroy service
--resize ID Resize service (requires -v) --resize ID Resize service (requires -v)
--redeploy ID Re-run bootstrap (with optional --bootstrap, --bootstrap-file, -f)
--execute ID Execute command in service --execute ID Execute command in service
--command CMD Command to execute (with --execute) --command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script --dump-bootstrap ID Dump bootstrap script