Add --dump-bootstrap command to remaining 28 un-inception implementations

This commit is contained in:
Russell Ballestrini 2025-12-27 17:14:36 -05:00
parent ee15bc8a5f
commit dba8ffeedc
33 changed files with 1636 additions and 109 deletions

76
Un.cs
View file

@ -406,6 +406,62 @@ class Un
return;
}
if (args.ServiceExecute != null)
{
var payload = new Dictionary<string, object>
{
["command"] = args.ServiceCommand
};
var result = ApiRequest($"/services/{args.ServiceExecute}/execute", "POST", payload, apiKey);
if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"]))
{
Console.Write($"{BLUE}{result["stdout"]}{RESET}");
}
if (result.ContainsKey("stderr") && !string.IsNullOrEmpty((string)result["stderr"]))
{
Console.Error.Write($"{RED}{result["stderr"]}{RESET}");
}
return;
}
if (args.ServiceDumpBootstrap != null)
{
Console.Error.WriteLine($"Fetching bootstrap script from {args.ServiceDumpBootstrap}...");
var payload = new Dictionary<string, object>
{
["command"] = "cat /tmp/bootstrap.sh"
};
var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", "POST", payload, apiKey);
var bootstrap = result.ContainsKey("stdout") ? (string)result["stdout"] : null;
if (!string.IsNullOrEmpty(bootstrap))
{
if (args.ServiceDumpFile != null)
{
try
{
File.WriteAllText(args.ServiceDumpFile, bootstrap);
Console.WriteLine($"Bootstrap saved to {args.ServiceDumpFile}");
}
catch (Exception e)
{
Console.Error.WriteLine($"{RED}Error: Could not write to {args.ServiceDumpFile}: {e.Message}{RESET}");
Environment.Exit(1);
}
}
else
{
Console.Write(bootstrap);
}
}
else
{
Console.Error.WriteLine($"{RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file){RESET}");
Environment.Exit(1);
}
return;
}
if (args.ServiceName != null)
{
var payload = new Dictionary<string, object>
@ -770,6 +826,10 @@ class Un
public string ServiceWake = null;
public string ServiceDestroy = null;
public string ServiceType = null;
public string ServiceExecute = null;
public string ServiceCommand = null;
public string ServiceDumpBootstrap = null;
public string ServiceDumpFile = null;
public bool KeyExtend = false;
}
@ -803,9 +863,13 @@ class Un
else if (arg == "--info") result.ServiceInfo = args[++i];
else if (arg == "--logs") result.ServiceLogs = args[++i];
else if (arg == "--tail") result.ServiceTail = args[++i];
else if (arg == "--sleep") result.ServiceSleep = args[++i];
else if (arg == "--wake") result.ServiceWake = args[++i];
else if (arg == "--freeze") result.ServiceSleep = args[++i];
else if (arg == "--unfreeze") result.ServiceWake = args[++i];
else if (arg == "--destroy") result.ServiceDestroy = args[++i];
else if (arg == "--execute") result.ServiceExecute = args[++i];
else if (arg == "--command") result.ServiceCommand = args[++i];
else if (arg == "--dump-bootstrap") result.ServiceDumpBootstrap = args[++i];
else if (arg == "--dump-file") result.ServiceDumpFile = args[++i];
else if (arg == "--extend") result.KeyExtend = true;
else if (!arg.StartsWith("-")) result.SourceFile = arg;
}
@ -842,9 +906,13 @@ Service options:
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--sleep ID Freeze service
--wake ID Unfreeze service
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
Key options:
--extend Open browser to extend expired key");

67
Un.java
View file

@ -274,6 +274,49 @@ public class Un {
return;
}
if (args.serviceExecute != null) {
Map<String, Object> payload = new HashMap<>();
payload.put("command", args.serviceCommand);
Map<String, Object> result = apiRequest("/services/" + args.serviceExecute + "/execute", "POST", payload, apiKey);
String stdout = (String) result.get("stdout");
String stderr = (String) result.get("stderr");
if (stdout != null && !stdout.isEmpty()) {
System.out.print(BLUE + stdout + RESET);
}
if (stderr != null && !stderr.isEmpty()) {
System.err.print(RED + stderr + RESET);
}
return;
}
if (args.serviceDumpBootstrap != null) {
System.err.println("Fetching bootstrap script from " + args.serviceDumpBootstrap + "...");
Map<String, Object> payload = new HashMap<>();
payload.put("command", "cat /tmp/bootstrap.sh");
Map<String, Object> result = apiRequest("/services/" + args.serviceDumpBootstrap + "/execute", "POST", payload, apiKey);
String bootstrap = (String) result.get("stdout");
if (bootstrap != null && !bootstrap.isEmpty()) {
if (args.serviceDumpFile != null) {
try {
java.nio.file.Path path = java.nio.file.Paths.get(args.serviceDumpFile);
java.nio.file.Files.write(path, bootstrap.getBytes());
path.toFile().setExecutable(true, false);
System.out.println("Bootstrap saved to " + args.serviceDumpFile);
} catch (Exception e) {
System.err.println(RED + "Error: Could not write to " + args.serviceDumpFile + ": " + e.getMessage() + RESET);
System.exit(1);
}
} else {
System.out.print(bootstrap);
}
} else {
System.err.println(RED + "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" + RESET);
System.exit(1);
}
return;
}
if (args.serviceName != null) {
Map<String, Object> payload = new HashMap<>();
payload.put("name", args.serviceName);
@ -667,6 +710,10 @@ public class Un {
String serviceSleep = null;
String serviceWake = null;
String serviceDestroy = null;
String serviceExecute = null;
String serviceCommand = null;
String serviceDumpBootstrap = null;
String serviceDumpFile = null;
// Key args
boolean keyExtend = false;
@ -717,12 +764,20 @@ public class Un {
result.serviceLogs = args[++i];
} else if (arg.equals("--tail")) {
result.serviceTail = args[++i];
} else if (arg.equals("--sleep")) {
} else if (arg.equals("--freeze")) {
result.serviceSleep = args[++i];
} else if (arg.equals("--wake")) {
} else if (arg.equals("--unfreeze")) {
result.serviceWake = args[++i];
} else if (arg.equals("--destroy")) {
result.serviceDestroy = args[++i];
} else if (arg.equals("--execute")) {
result.serviceExecute = args[++i];
} else if (arg.equals("--command")) {
result.serviceCommand = args[++i];
} else if (arg.equals("--dump-bootstrap")) {
result.serviceDumpBootstrap = args[++i];
} else if (arg.equals("--dump-file")) {
result.serviceDumpFile = args[++i];
} else if (arg.equals("--extend")) {
result.keyExtend = true;
} else if (!arg.startsWith("-")) {
@ -761,9 +816,13 @@ public class Un {
System.out.println(" --info ID Get service details");
System.out.println(" --logs ID Get all logs");
System.out.println(" --tail ID Get last 9000 lines");
System.out.println(" --sleep ID Freeze service");
System.out.println(" --wake ID Unfreeze service");
System.out.println(" --freeze ID Freeze service");
System.out.println(" --unfreeze ID Unfreeze service");
System.out.println(" --destroy ID Destroy service");
System.out.println(" --execute ID Execute command in service");
System.out.println(" --command CMD Command to execute (with --execute)");
System.out.println(" --dump-bootstrap ID Dump bootstrap script");
System.out.println(" --dump-file FILE File to save bootstrap (with --dump-bootstrap)");
System.out.println();
System.out.println("Key options:");
System.out.println(" --extend Open browser to extend key");

50
un.awk
View file

@ -188,6 +188,47 @@ function service_destroy(id) {
print GREEN "Service destroyed: " id RESET
}
function service_dump_bootstrap(id, dump_file) {
api_key = get_api_key()
print "Fetching bootstrap script from " id "..." > "/dev/stderr"
# Build the curl command to execute on the service
cmd = "curl -s -X POST '" API_BASE "/services/" id "/execute' " \
"-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"
response = ""
while ((cmd | getline line) > 0) {
response = response line
}
close(cmd)
# Parse stdout from response
if (match(response, /"stdout":"([^"]*)"/, arr)) {
stdout = arr[1]
# Unescape JSON
gsub(/\\n/, "\n", stdout)
gsub(/\\t/, "\t", stdout)
gsub(/\\"/, "\"", stdout)
gsub(/\\\\/, "\\", stdout)
if (dump_file != "") {
# Write to file
print stdout > dump_file
close(dump_file)
system("chmod 755 " dump_file)
print "Bootstrap saved to " dump_file
} else {
# Print to stdout
printf "%s", stdout
}
} else {
print RED "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" RESET > "/dev/stderr"
exit 1
}
}
function service_create(name, ports, domains, service_type, bootstrap) {
api_key = get_api_key()
@ -337,6 +378,7 @@ function show_help() {
print " awk -f un.awk service --list"
print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD]"
print " awk -f un.awk service --destroy ID"
print " awk -f un.awk service --dump-bootstrap ID [--dump-file FILE]"
print ""
print "Service options:"
print " --name NAME Service name (required for --create)"
@ -344,6 +386,8 @@ function show_help() {
print " --domains DOMAINS Comma-separated domain names"
print " --type TYPE Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)"
print " --bootstrap CMD Bootstrap command or script"
print " --dump-bootstrap ID Dump bootstrap script from service"
print " --dump-file FILE Save bootstrap to file (with --dump-bootstrap)"
print ""
print "Requires: UNSANDBOX_API_KEY environment variable"
}
@ -390,6 +434,12 @@ END {
service_list()
} else if (ARGC >= 4 && ARGV[2] == "--destroy") {
service_destroy(ARGV[3])
} else if (ARGC >= 4 && ARGV[2] == "--dump-bootstrap") {
dump_file = ""
if (ARGC >= 6 && ARGV[4] == "--dump-file") {
dump_file = ARGV[5]
}
service_dump_bootstrap(ARGV[3], dump_file)
} else if (ARGV[2] == "--create") {
# Parse service creation arguments
name = ""

41
un.clj
View file

@ -201,6 +201,31 @@
:destroy (do
(curl-delete api-key (str "/services/" sid))
(println (str green "Service destroyed: " sid reset)))
:execute (when (and sid bootstrap)
(let [json (str "{\"command\":\"" (escape-json bootstrap) "\"}")
response (curl-post api-key (str "/services/" sid "/execute") json)
stdout-val (extract-field "stdout" response)]
(when stdout-val
(print (str blue (unescape-json stdout-val) reset))
(flush))))
:dump-bootstrap (when sid
(binding [*out* *err*]
(println (str "Fetching bootstrap script from " sid "...")))
(let [json "{\"command\":\"cat /tmp/bootstrap.sh\"}"
response (curl-post api-key (str "/services/" sid "/execute") json)
stdout-val (extract-field "stdout" response)]
(if stdout-val
(let [script (unescape-json stdout-val)]
(if service-type
(do
(spit service-type script)
(sh "chmod" "755" service-type)
(println (str "Bootstrap saved to " service-type)))
(print script)))
(do
(binding [*out* *err*]
(println (str red "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" reset)))
(System/exit 1)))))
:create (when name
(let [ports-json (if ports (str ",\"ports\":[" ports "]") "")
bootstrap-json (if bootstrap (str ",\"bootstrap\":\"" (escape-json bootstrap) "\"") "")
@ -336,11 +361,11 @@
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:logs (second args) service-name service-ports service-bootstrap service-type key-extend mode)
(and (= mode :service) (= (first args) "--sleep"))
(and (= mode :service) (= (first args) "--freeze"))
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:sleep (second args) service-name service-ports service-bootstrap service-type key-extend mode)
(and (= mode :service) (= (first args) "--wake"))
(and (= mode :service) (= (first args) "--unfreeze"))
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:wake (second args) service-name service-ports service-bootstrap service-type key-extend mode)
@ -348,6 +373,18 @@
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:destroy (second args) service-name service-ports service-bootstrap service-type key-extend mode)
(and (= mode :service) (= (first args) "--execute"))
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:execute (second args) service-name service-ports (nth args 2) service-type key-extend mode)
(and (= mode :service) (= (first args) "--dump-bootstrap") (>= (count args) 3) (not (.startsWith (nth args 2) "-")))
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:dump-bootstrap (second args) service-name service-ports service-bootstrap (nth args 2) key-extend mode)
(and (= mode :service) (= (first args) "--dump-bootstrap"))
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:dump-bootstrap (second args) service-name service-ports service-bootstrap service-type key-extend mode)
(and (= mode :service) (= (first args) "--name"))
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
:create service-id (second args) service-ports service-bootstrap service-type key-extend mode)

55
un.cob
View file

@ -197,22 +197,25 @@
ELSE IF WS-ARG2 = "--logs"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM SERVICE-LOGS
ELSE IF WS-ARG2 = "--sleep"
ELSE IF WS-ARG2 = "--freeze"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM SERVICE-SLEEP
ELSE IF WS-ARG2 = "--wake"
ELSE IF WS-ARG2 = "--unfreeze"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM SERVICE-WAKE
ELSE IF WS-ARG2 = "--destroy"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM SERVICE-DESTROY
ELSE IF WS-ARG2 = "--dump-bootstrap"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM SERVICE-DUMP-BOOTSTRAP
ELSE IF WS-ARG2 = "--name"
ACCEPT WS-NAME FROM ARGUMENT-VALUE
PERFORM PARSE-SERVICE-CREATE-ARGS
PERFORM SERVICE-CREATE
ELSE
DISPLAY "Error: Use --list, --info, --logs, "
"--sleep, --wake, --destroy, or --name" UPON SYSERR
"--freeze, --unfreeze, --destroy, --dump-bootstrap, or --name" UPON SYSERR
MOVE 1 TO RETURN-CODE
END-IF.
@ -375,6 +378,52 @@
CALL "SYSTEM" USING WS-CURL-CMD.
SERVICE-DUMP-BOOTSTRAP.
* Check if WS-ARG3 contains --dump-file argument
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE.
MOVE SPACES TO WS-BOOTSTRAP.
IF WS-ARG3 = "--dump-file"
ACCEPT WS-BOOTSTRAP FROM ARGUMENT-VALUE
END-IF.
STRING "echo 'Fetching bootstrap script from "
FUNCTION TRIM(WS-ID) "...' >&2; "
"RESP=$(curl -s -X POST "
"https://api.unsandbox.com/services/"
FUNCTION TRIM(WS-ID) "/execute "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY)
"' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}'); "
"STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty'); "
"if [ -n \"$STDOUT\" ]; then "
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING.
IF WS-BOOTSTRAP NOT = SPACES
STRING FUNCTION TRIM(WS-CURL-CMD)
"echo \"$STDOUT\" > '"
FUNCTION TRIM(WS-BOOTSTRAP)
"' && chmod 755 '"
FUNCTION TRIM(WS-BOOTSTRAP)
"' && echo 'Bootstrap saved to "
FUNCTION TRIM(WS-BOOTSTRAP) "'; "
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING
ELSE
STRING FUNCTION TRIM(WS-CURL-CMD)
"echo \"$STDOUT\"; "
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING
END-IF.
STRING FUNCTION TRIM(WS-CURL-CMD)
"else echo -e '\x1b[31mError: Failed to fetch "
"bootstrap\x1b[0m' >&2; exit 1; fi"
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING.
CALL "SYSTEM" USING WS-CURL-CMD.
PARSE-SERVICE-CREATE-ARGS.
* Parse remaining arguments for service creation
* This is a simplified parser that looks for specific flags

101
un.cpp
View file

@ -218,7 +218,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
cout << exec_curl(cmd) << endl;
}
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& network, int vcpu, const string& api_key) {
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const string& api_key) {
if (list) {
string cmd = "curl -s -X GET '" + API_BASE + "/services' -H 'Authorization: Bearer " + api_key + "'";
cout << exec_curl(cmd) << endl;
@ -264,6 +264,93 @@ void cmd_service(const string& name, const string& ports, const string& type, co
return;
}
if (!execute.empty()) {
ostringstream json;
json << "{\"command\":\"" << escape_json(command) << "\"}";
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + execute + "/execute' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
"-d '" + json.str() + "'";
string result = exec_curl(cmd);
size_t stdout_pos = result.find("\"stdout\":\"");
size_t stderr_pos = result.find("\"stderr\":\"");
if (stdout_pos != string::npos) {
stdout_pos += 10;
size_t end = result.find("\"", stdout_pos);
while (end > 0 && result[end-1] == '\\') end = result.find("\"", end+1);
if (end != string::npos) {
string out = result.substr(stdout_pos, end - stdout_pos);
size_t pos = 0;
while ((pos = out.find("\\n", pos)) != string::npos) {
out.replace(pos, 2, "\n");
}
cout << BLUE << out << RESET;
}
}
if (stderr_pos != string::npos) {
stderr_pos += 10;
size_t end = result.find("\"", stderr_pos);
while (end > 0 && result[end-1] == '\\') end = result.find("\"", end+1);
if (end != string::npos) {
string err = result.substr(stderr_pos, end - stderr_pos);
size_t pos = 0;
while ((pos = err.find("\\n", pos)) != string::npos) {
err.replace(pos, 2, "\n");
}
cerr << RED << err << RESET;
}
}
return;
}
if (!dump_bootstrap.empty()) {
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + dump_bootstrap + "/execute' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'";
string result = exec_curl(cmd);
size_t stdout_pos = result.find("\"stdout\":\"");
if (stdout_pos != string::npos) {
stdout_pos += 10;
size_t end = result.find("\"", stdout_pos);
while (end > 0 && result[end-1] == '\\') end = result.find("\"", end+1);
if (end != string::npos) {
string bootstrap_script = result.substr(stdout_pos, end - stdout_pos);
size_t pos = 0;
while ((pos = bootstrap_script.find("\\n", pos)) != string::npos) {
bootstrap_script.replace(pos, 2, "\n");
}
if (!dump_file.empty()) {
ofstream outfile(dump_file);
if (outfile) {
outfile << bootstrap_script;
outfile.close();
chmod(dump_file.c_str(), 0755);
cout << "Bootstrap saved to " << dump_file << endl;
} else {
cerr << RED << "Error: Could not write to " << dump_file << RESET << endl;
exit(1);
}
} else {
cout << bootstrap_script;
}
} else {
cerr << RED << "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" << RESET << endl;
exit(1);
}
} else {
cerr << RED << "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" << RESET << endl;
exit(1);
}
return;
}
if (!name.empty()) {
ostringstream json;
json << "{\"name\":\"" << name << "\"";
@ -414,7 +501,7 @@ int main(int argc, char* argv[]) {
if (cmd_type == "service") {
string name, ports, type, bootstrap;
bool list = false;
string info, logs, tail, sleep, wake, destroy, network;
string info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network;
int vcpu = 0;
for (int i = 2; i < argc; i++) {
@ -427,15 +514,19 @@ int main(int argc, char* argv[]) {
else if (arg == "--info" && i+1 < argc) info = argv[++i];
else if (arg == "--logs" && i+1 < argc) logs = argv[++i];
else if (arg == "--tail" && i+1 < argc) tail = argv[++i];
else if (arg == "--sleep" && i+1 < argc) sleep = argv[++i];
else if (arg == "--wake" && i+1 < argc) wake = argv[++i];
else if (arg == "--freeze" && i+1 < argc) sleep = argv[++i];
else if (arg == "--unfreeze" && i+1 < argc) wake = argv[++i];
else if (arg == "--destroy" && i+1 < argc) destroy = 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 == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = 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 == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
else if (arg == "-k" && i+1 < argc) api_key = argv[++i];
}
cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, network, vcpu, api_key);
cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, api_key);
return 0;
}

46
un.cr
View file

@ -363,6 +363,39 @@ def cmd_service(args)
return
end
if execute_id = args[:execute]?.as?(String)
command = args[:command]?.as?(String) || ""
payload = JSON.parse({command: command}.to_json)
result = api_request("/services/#{execute_id}/execute", api_key, method: "POST", data: payload)
if stdout = result["stdout"]?.try(&.as_s?)
print BLUE, stdout, RESET
end
if stderr = result["stderr"]?.try(&.as_s?)
print RED, stderr, RESET
end
return
end
if dump_id = args[:dump_bootstrap]?.as?(String)
STDERR.puts "Fetching bootstrap script from #{dump_id}..."
payload = JSON.parse({command: "cat /tmp/bootstrap.sh"}.to_json)
result = api_request("/services/#{dump_id}/execute", api_key, method: "POST", data: payload)
if bootstrap = result["stdout"]?.try(&.as_s?)
if file_path = args[:dump_file]?.as?(String)
File.write(file_path, bootstrap)
File.chmod(file_path, 0o755)
puts "Bootstrap saved to #{file_path}"
else
print bootstrap
end
else
STDERR.puts "#{RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{RESET}"
exit 1
end
return
end
# Create new service
if name = args[:name]?.as?(String)
payload = JSON.parse({name: name}.to_json)
@ -409,7 +442,7 @@ def cmd_service(args)
return
end
STDERR.puts "#{RED}Error: Use --list, --info, --logs, --sleep, --wake, --destroy, or --name to create#{RESET}"
STDERR.puts "#{RED}Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --name to create#{RESET}"
exit 1
end
@ -430,6 +463,9 @@ def main
sleep: nil,
wake: nil,
destroy: nil,
execute: nil,
dump_bootstrap: nil,
dump_file: nil,
name: nil,
ports: nil,
domains: nil,
@ -451,9 +487,13 @@ def main
opts.on("--kill=ID", "Kill session") { |id| args[:kill] = id }
opts.on("--info=ID", "Get service info") { |id| args[:info] = id }
opts.on("--logs=ID", "Get service logs") { |id| args[:logs] = id }
opts.on("--sleep=ID", "Sleep service") { |id| args[:sleep] = id }
opts.on("--wake=ID", "Wake service") { |id| args[:wake] = id }
opts.on("--freeze=ID", "Sleep service") { |id| args[:sleep] = id }
opts.on("--unfreeze=ID", "Wake service") { |id| args[:wake] = id }
opts.on("--destroy=ID", "Destroy service") { |id| args[:destroy] = id }
opts.on("--execute=ID", "Execute command in service") { |id| args[:execute] = id }
opts.on("--command=CMD", "Command to execute (with --execute)") { |cmd| args[:command] = cmd }
opts.on("--dump-bootstrap=ID", "Dump bootstrap script") { |id| args[:dump_bootstrap] = id }
opts.on("--dump-file=FILE", "File to save bootstrap (with --dump-bootstrap)") { |file| args[:dump_file] = file }
opts.on("--name=NAME", "Service name") { |n| args[:name] = n }
opts.on("--ports=PORTS", "Comma-separated ports") { |p| args[:ports] = p }
opts.on("--domains=DOMAINS", "Comma-separated domains") { |d| args[:domains] = d }

82
un.d
View file

@ -152,7 +152,7 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
writeln(execCurl(cmd));
}
void cmdService(string name, string ports, string bootstrap, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string network, int vcpu, string apiKey) {
void cmdService(string name, string ports, string bootstrap, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string apiKey) {
if (list) {
string cmd = format(`curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'`, API_BASE, apiKey);
writeln(execCurl(cmd));
@ -198,6 +198,74 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
return;
}
if (!execute.empty) {
string json = format(`{"command":"%s"}`, escapeJson(command));
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, execute, apiKey, json);
string result = execCurl(cmd);
// Simple JSON parsing for stdout/stderr
import std.algorithm : findSplitAfter;
auto stdoutSearch = result.findSplitAfter(`"stdout":"`);
if (stdoutSearch[0].length > 0 && stdoutSearch[1].length > 0) {
auto stdoutEnd = stdoutSearch[1].findSplitAfter(`"`);
if (stdoutEnd[0].length > 1) {
string output = stdoutEnd[0][0..$-1];
output = output.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\");
write(output);
}
}
auto stderrSearch = result.findSplitAfter(`"stderr":"`);
if (stderrSearch[0].length > 0 && stderrSearch[1].length > 0) {
auto stderrEnd = stderrSearch[1].findSplitAfter(`"`);
if (stderrEnd[0].length > 1) {
string errout = stderrEnd[0][0..$-1];
errout = errout.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\");
stderr.write(errout);
}
}
return;
}
if (!dumpBootstrap.empty) {
stderr.writefln("Fetching bootstrap script from %s...", dumpBootstrap);
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{"command":"cat /tmp/bootstrap.sh"}'`, API_BASE, dumpBootstrap, apiKey);
string result = execCurl(cmd);
import std.algorithm : findSplitAfter;
auto stdoutSearch = result.findSplitAfter(`"stdout":"`);
if (stdoutSearch[0].length > 0 && stdoutSearch[1].length > 0) {
auto stdoutEnd = stdoutSearch[1].findSplitAfter(`"`);
if (stdoutEnd[0].length > 1) {
string bootstrapScript = stdoutEnd[0][0..$-1];
bootstrapScript = bootstrapScript.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\");
if (!dumpFile.empty) {
try {
std.file.write(dumpFile, bootstrapScript);
version(Posix) {
import core.sys.posix.sys.stat;
chmod(dumpFile.toStringz(), octal!755);
}
writefln("Bootstrap saved to %s", dumpFile);
} catch (Exception e) {
stderr.writefln("%sError: Could not write to %s: %s%s", RED, dumpFile, e.msg, RESET);
exit(1);
}
} else {
write(bootstrapScript);
}
} else {
stderr.writefln("%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s", RED, RESET);
exit(1);
}
} else {
stderr.writefln("%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s", RED, RESET);
exit(1);
}
return;
}
if (!name.empty) {
string json = format(`{"name":"%s"`, name);
if (!ports.empty) json ~= format(`,"ports":[%s]`, ports);
@ -379,7 +447,7 @@ int main(string[] args) {
if (args[1] == "service") {
string name, ports, bootstrap, type;
bool list = false;
string info, logs, tail, sleep, wake, destroy, network;
string info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network;
int vcpu = 0;
for (size_t i = 2; i < args.length; i++) {
@ -391,15 +459,19 @@ int main(string[] args) {
else if (args[i] == "--info" && i+1 < args.length) info = args[++i];
else if (args[i] == "--logs" && i+1 < args.length) logs = args[++i];
else if (args[i] == "--tail" && i+1 < args.length) tail = args[++i];
else if (args[i] == "--sleep" && i+1 < args.length) sleep = args[++i];
else if (args[i] == "--wake" && i+1 < args.length) wake = args[++i];
else if (args[i] == "--freeze" && i+1 < args.length) sleep = args[++i];
else if (args[i] == "--unfreeze" && i+1 < args.length) wake = args[++i];
else if (args[i] == "--destroy" && i+1 < args.length) destroy = args[++i];
else if (args[i] == "--execute" && i+1 < args.length) execute = args[++i];
else if (args[i] == "--command" && i+1 < args.length) command = args[++i];
else if (args[i] == "--dump-bootstrap" && i+1 < args.length) dumpBootstrap = args[++i];
else if (args[i] == "--dump-file" && i+1 < args.length) dumpFile = args[++i];
else if (args[i] == "-n" && i+1 < args.length) network = args[++i];
else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]);
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i];
}
cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, network, vcpu, apiKey);
cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, apiKey);
return 0;
}

72
un.dart
View file

@ -91,6 +91,10 @@ class Args {
String? serviceSleep;
String? serviceWake;
String? serviceDestroy;
String? serviceExecute;
String? serviceCommand;
String? serviceDumpBootstrap;
String? serviceDumpFile;
bool keyExtend = false;
}
@ -318,6 +322,50 @@ Future<void> cmdService(Args args) async {
return;
}
if (args.serviceExecute != null) {
final payload = <String, dynamic>{
'command': args.serviceCommand,
};
final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), apiKey);
final stdoutText = result['stdout'] as String?;
final stderrText = result['stderr'] as String?;
if (stdoutText != null && stdoutText.isNotEmpty) {
stdout.write('$blue$stdoutText$reset');
}
if (stderrText != null && stderrText.isNotEmpty) {
stderr.write('$red$stderrText$reset');
}
return;
}
if (args.serviceDumpBootstrap != null) {
stderr.writeln('Fetching bootstrap script from ${args.serviceDumpBootstrap}...');
final payload = <String, dynamic>{
'command': 'cat /tmp/bootstrap.sh',
};
final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), apiKey);
final bootstrap = result['stdout'] as String?;
if (bootstrap != null && bootstrap.isNotEmpty) {
if (args.serviceDumpFile != null) {
try {
await File(args.serviceDumpFile!).writeAsString(bootstrap);
await Process.run('chmod', ['755', args.serviceDumpFile!]);
print('Bootstrap saved to ${args.serviceDumpFile}');
} catch (e) {
stderr.writeln('${red}Error: Could not write to ${args.serviceDumpFile}: $e$reset');
exit(1);
}
} else {
stdout.write(bootstrap);
}
} else {
stderr.writeln('${red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)$reset');
exit(1);
}
return;
}
if (args.serviceName != null) {
final payload = <String, dynamic>{
'name': args.serviceName!,
@ -485,15 +533,27 @@ Args parseArgs(List<String> argv) {
case '--tail':
args.serviceTail = argv[++i];
break;
case '--sleep':
case '--freeze':
args.serviceSleep = argv[++i];
break;
case '--wake':
case '--unfreeze':
args.serviceWake = argv[++i];
break;
case '--destroy':
args.serviceDestroy = argv[++i];
break;
case '--execute':
args.serviceExecute = argv[++i];
break;
case '--command':
args.serviceCommand = argv[++i];
break;
case '--dump-bootstrap':
args.serviceDumpBootstrap = argv[++i];
break;
case '--dump-file':
args.serviceDumpFile = argv[++i];
break;
case '--extend':
args.keyExtend = true;
break;
@ -537,9 +597,13 @@ Service options:
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--sleep ID Freeze service
--wake ID Unfreeze service
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
Key options:
--extend Open browser to extend key

47
un.erl
View file

@ -128,14 +128,14 @@ service_command(["--logs", ServiceId | _]) ->
Response = curl_get(ApiKey, "/services/" ++ ServiceId ++ "/logs"),
io:format("~s~n", [Response]);
service_command(["--sleep", ServiceId | _]) ->
service_command(["--freeze", ServiceId | _]) ->
ApiKey = get_api_key(),
TmpFile = write_temp_file("{}"),
_ = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/sleep", TmpFile),
file:delete(TmpFile),
io:format("\033[32mService sleeping: ~s\033[0m~n", [ServiceId]);
service_command(["--wake", ServiceId | _]) ->
service_command(["--unfreeze", ServiceId | _]) ->
ApiKey = get_api_key(),
TmpFile = write_temp_file("{}"),
_ = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/wake", TmpFile),
@ -147,6 +147,49 @@ service_command(["--destroy", ServiceId | _]) ->
_ = curl_delete(ApiKey, "/services/" ++ ServiceId),
io:format("\033[32mService destroyed: ~s\033[0m~n", [ServiceId]);
service_command(["--execute", ServiceId, "--command", Command | _]) ->
ApiKey = get_api_key(),
Json = "{\"command\":\"" ++ escape_json(Command) ++ "\"}",
TmpFile = write_temp_file(Json),
Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/execute", TmpFile),
file:delete(TmpFile),
case extract_json_field(Response, "stdout") of
"" -> ok;
Stdout -> io:format("\033[34m~s\033[0m", [Stdout])
end;
service_command(["--dump-bootstrap", ServiceId, File | _]) ->
ApiKey = get_api_key(),
io:format(standard_error, "Fetching bootstrap script from ~s...~n", [ServiceId]),
Json = "{\"command\":\"cat /tmp/bootstrap.sh\"}",
TmpFile = write_temp_file(Json),
Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/execute", TmpFile),
file:delete(TmpFile),
case extract_json_field(Response, "stdout") of
"" ->
io:format(standard_error, "\033[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\033[0m~n"),
halt(1);
Script ->
file:write_file(File, Script),
os:cmd("chmod 755 " ++ File),
io:format("Bootstrap saved to ~s~n", [File])
end;
service_command(["--dump-bootstrap", ServiceId | _]) ->
ApiKey = get_api_key(),
io:format(standard_error, "Fetching bootstrap script from ~s...~n", [ServiceId]),
Json = "{\"command\":\"cat /tmp/bootstrap.sh\"}",
TmpFile = write_temp_file(Json),
Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/execute", TmpFile),
file:delete(TmpFile),
case extract_json_field(Response, "stdout") of
"" ->
io:format(standard_error, "\033[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\033[0m~n"),
halt(1);
Script ->
io:format("~s", [Script])
end;
service_command(Args) ->
case get_service_name(Args) of
undefined ->

47
un.ex
View file

@ -166,13 +166,13 @@ defmodule Un do
IO.puts(response)
end
defp service_command(["--sleep", service_id | _]) do
defp service_command(["--freeze", service_id | _]) do
api_key = get_api_key()
curl_post(api_key, "/services/#{service_id}/sleep", "{}")
IO.puts("#{@green}Service sleeping: #{service_id}#{@reset}")
end
defp service_command(["--wake", service_id | _]) do
defp service_command(["--unfreeze", service_id | _]) do
api_key = get_api_key()
curl_post(api_key, "/services/#{service_id}/wake", "{}")
IO.puts("#{@green}Service waking: #{service_id}#{@reset}")
@ -184,6 +184,49 @@ defmodule Un do
IO.puts("#{@green}Service destroyed: #{service_id}#{@reset}")
end
defp service_command(["--execute", service_id, "--command", command | _]) do
api_key = get_api_key()
json = "{\"command\":\"#{escape_json(command)}\"}"
response = curl_post(api_key, "/services/#{service_id}/execute", json)
case extract_json_value(response, "stdout") do
nil -> :ok
stdout -> IO.write("#{@blue}#{stdout}#{@reset}")
end
end
defp service_command(["--dump-bootstrap", service_id, file | _]) do
api_key = get_api_key()
IO.puts(:stderr, "Fetching bootstrap script from #{service_id}...")
json = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
response = curl_post(api_key, "/services/#{service_id}/execute", json)
case extract_json_value(response, "stdout") do
nil ->
IO.puts(:stderr, "#{@red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{@reset}")
System.halt(1)
script ->
File.write!(file, script)
System.cmd("chmod", ["755", file])
IO.puts("Bootstrap saved to #{file}")
end
end
defp service_command(["--dump-bootstrap", service_id | _]) do
api_key = get_api_key()
IO.puts(:stderr, "Fetching bootstrap script from #{service_id}...")
json = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
response = curl_post(api_key, "/services/#{service_id}/execute", json)
case extract_json_value(response, "stdout") do
nil ->
IO.puts(:stderr, "#{@red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{@reset}")
System.halt(1)
script ->
IO.write(script)
end
end
defp service_command(args) do
name = get_opt(args, "--name", nil, nil)

32
un.f90
View file

@ -248,12 +248,12 @@ contains
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
end if
else if (trim(arg) == '--sleep') then
else if (trim(arg) == '--freeze') then
operation = 'sleep'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
end if
else if (trim(arg) == '--wake') then
else if (trim(arg) == '--unfreeze') then
operation = 'wake'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
@ -263,6 +263,16 @@ contains
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
end if
else if (trim(arg) == '--dump-bootstrap') then
operation = 'dump-bootstrap'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
end if
else if (trim(arg) == '--dump-file') then
operation = 'dump-file'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_type)
end if
end if
end do
@ -314,8 +324,24 @@ contains
'-H "Authorization: Bearer ', trim(api_key), '" >/dev/null && ', &
'echo -e "\x1b[32mService destroyed: ', trim(service_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'dump-bootstrap' .and. len_trim(service_id) > 0) then
write(full_cmd, '(20A)') &
'echo "Fetching bootstrap script from ', trim(service_id), '..." >&2; ', &
'RESP=$(curl -s -X POST https://api.unsandbox.com/services/', &
trim(service_id), '/execute ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(api_key), '" ', &
'-d ''{"command":"cat /tmp/bootstrap.sh"}''); ', &
'STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); ', &
'if [ -n "$STDOUT" ]; then ', &
'if [ -n "', trim(service_type), '" ]; then ', &
'echo "$STDOUT" > "', trim(service_type), '" && chmod 755 "', trim(service_type), '" && ', &
'echo "Bootstrap saved to ', trim(service_type), '"; ', &
'else echo "$STDOUT"; fi; ', &
'else echo -e "\x1b[31mError: Failed to fetch bootstrap\x1b[0m" >&2; exit 1; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else
write(0, '(A)') 'Error: Use --list, --info, --logs, --sleep, --wake, or --destroy'
write(0, '(A)') 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --dump-bootstrap'
stop 1
end if
end subroutine handle_service

View file

@ -258,6 +258,43 @@
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
;
\ Service dump bootstrap
: service-dump-bootstrap ( service-id-addr service-id-len file-addr file-len -- )
get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw
s" echo 'Fetching bootstrap script from " r@ write-file throw
2over r@ write-file throw
s" ...' >&2" r@ write-line throw
s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw
2over r@ write-file throw
s" /execute -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}')" r@ write-line throw
s" STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty')" r@ write-line throw
s" if [ -n \"$STDOUT\" ]; then" r@ write-line throw
2dup 0 0 d= if
\ No file specified, print to stdout
2drop
s" echo \"$STDOUT\"" r@ write-line throw
else
\ File specified, save to file
s" echo \"$STDOUT\" > '" r@ write-file throw
r@ write-file throw
s" ' && chmod 755 '" r@ write-file throw
2dup r@ write-file throw
s" ' && echo 'Bootstrap saved to " r@ write-file throw
r@ write-file throw
s" '" r@ write-line throw
then
s" else" r@ write-line throw
s" echo -e '\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m' >&2" r@ write-line throw
s" exit 1" r@ write-line throw
s" fi" r@ write-line throw
r> close-file throw
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
;
\ Service create (requires --name, optional --ports, --domains, --type, --bootstrap)
: service-create ( -- )
get-api-key
@ -394,7 +431,7 @@
\ Handle service subcommand
: handle-service ( -- )
argc @ 3 < if
s" Error: Use --name (create), --list, --info, --logs, --sleep, --wake, or --destroy" type cr
s" Error: Use --name (create), --list, --info, --logs, --freeze, --unfreeze, or --destroy" type cr
1 (bye)
then
@ -433,20 +470,20 @@
0 (bye)
then
2dup s" --sleep" compare 0= if
2dup s" --freeze" compare 0= if
2drop
argc @ 4 < if
s" Error: --sleep requires service ID" type cr
s" Error: --freeze requires service ID" type cr
1 (bye)
then
3 arg service-sleep
0 (bye)
then
2dup s" --wake" compare 0= if
2dup s" --unfreeze" compare 0= if
2drop
argc @ 4 < if
s" Error: --wake requires service ID" type cr
s" Error: --unfreeze requires service ID" type cr
1 (bye)
then
3 arg service-wake
@ -463,8 +500,34 @@
0 (bye)
then
2dup s" --dump-bootstrap" compare 0= if
2drop
argc @ 4 < if
s" Error: --dump-bootstrap requires service ID" type cr
1 (bye)
then
3 arg
\ Check for --dump-file
argc @ 5 >= if
4 arg 2dup s" --dump-file" compare 0= if
2drop
argc @ 6 < if
s" Error: --dump-file requires filename" type cr
1 (bye)
then
5 arg
else
2drop 0 0
then
else
0 0
then
service-dump-bootstrap
0 (bye)
then
2drop
s" Error: Use --name (create), --list, --info, --logs, --sleep, --wake, or --destroy" type cr
s" Error: Use --name (create), --list, --info, --logs, --freeze, --unfreeze, --destroy, or --dump-bootstrap" type cr
1 (bye)
;

55
un.fs
View file

@ -94,6 +94,10 @@ type Args = {
mutable ServiceSleep: string option
mutable ServiceWake: string option
mutable ServiceDestroy: string option
mutable ServiceExecute: string option
mutable ServiceCommand: string option
mutable ServiceDumpBootstrap: string option
mutable ServiceDumpFile: string option
mutable KeyExtend: bool
}
@ -436,6 +440,37 @@ let cmdService (args: Args) =
elif args.ServiceDestroy.IsSome then
let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None apiKey
printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value reset
elif args.ServiceExecute.IsSome then
let payload = [("command", box args.ServiceCommand.Value)]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) apiKey
match result.TryFind "stdout" with
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
printf "%s%s%s" blue (stdout.ToString()) reset
| _ -> ()
match result.TryFind "stderr" with
| Some stderr when not (String.IsNullOrEmpty(stderr.ToString())) ->
eprintf "%s%s%s" red (stderr.ToString()) reset
| _ -> ()
elif args.ServiceDumpBootstrap.IsSome then
eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value
let payload = [("command", box "cat /tmp/bootstrap.sh")]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) apiKey
match result.TryFind "stdout" with
| Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) ->
let bootstrapText = bootstrap.ToString()
if args.ServiceDumpFile.IsSome then
try
File.WriteAllText(args.ServiceDumpFile.Value, bootstrapText)
printfn "Bootstrap saved to %s" args.ServiceDumpFile.Value
with ex ->
eprintfn "%sError: Could not write to %s: %s%s" red args.ServiceDumpFile.Value ex.Message reset
exit 1
else
printf "%s" bootstrapText
| _ ->
eprintfn "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s" red reset
exit 1
elif args.ServiceName.IsSome then
let mutable payload = [("name", box args.ServiceName.Value)]
if args.ServicePorts.IsSome then
@ -489,6 +524,10 @@ let parseArgs (argv: string[]) =
ServiceSleep = None
ServiceWake = None
ServiceDestroy = None
ServiceExecute = None
ServiceCommand = None
ServiceDumpBootstrap = None
ServiceDumpFile = None
KeyExtend = false
}
@ -519,9 +558,13 @@ let parseArgs (argv: string[]) =
| "--info" -> i <- i + 1; args.ServiceInfo <- Some argv.[i]
| "--logs" -> i <- i + 1; args.ServiceLogs <- Some argv.[i]
| "--tail" -> i <- i + 1; args.ServiceTail <- Some argv.[i]
| "--sleep" -> i <- i + 1; args.ServiceSleep <- Some argv.[i]
| "--wake" -> i <- i + 1; args.ServiceWake <- Some argv.[i]
| "--freeze" -> i <- i + 1; args.ServiceSleep <- Some argv.[i]
| "--unfreeze" -> i <- i + 1; args.ServiceWake <- Some argv.[i]
| "--destroy" -> i <- i + 1; args.ServiceDestroy <- Some argv.[i]
| "--execute" -> i <- i + 1; args.ServiceExecute <- Some argv.[i]
| "--command" -> i <- i + 1; args.ServiceCommand <- Some argv.[i]
| "--dump-bootstrap" -> i <- i + 1; args.ServiceDumpBootstrap <- Some argv.[i]
| "--dump-file" -> i <- i + 1; args.ServiceDumpFile <- Some argv.[i]
| "--extend" -> args.KeyExtend <- true
| arg when not (arg.StartsWith("-")) -> args.SourceFile <- Some arg
| _ -> ()
@ -558,9 +601,13 @@ let printHelp () =
printfn " --info ID Get service details"
printfn " --logs ID Get all logs"
printfn " --tail ID Get last 9000 lines"
printfn " --sleep ID Freeze service"
printfn " --wake ID Unfreeze service"
printfn " --freeze ID Freeze service"
printfn " --unfreeze ID Unfreeze service"
printfn " --destroy ID Destroy service"
printfn " --execute ID Execute command in service"
printfn " --command CMD Command to execute (with --execute)"
printfn " --dump-bootstrap ID Dump bootstrap script"
printfn " --dump-file FILE File to save bootstrap (with --dump-bootstrap)"
printfn ""
printfn "Key options:"
printfn " --extend Open browser to extend key"

View file

@ -85,6 +85,10 @@ class Args {
String serviceSleep = null
String serviceWake = null
String serviceDestroy = null
String serviceExecute = null
String serviceCommand = null
String serviceDumpBootstrap = null
String serviceDumpFile = null
Boolean keyExtend = false
}
@ -408,6 +412,64 @@ def cmdService(args) {
return
}
if (args.serviceExecute) {
def json = """{"command":"${args.serviceCommand}"}"""
def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, apiKey)
def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/
def stderrMatch = output =~ /"stderr":"((?:[^"\\\\]|\\\\.)*)"/
if (stdoutMatch.find()) {
def stdout = stdoutMatch.group(1)
.replace('\\n', '\n')
.replace('\\t', '\t')
.replace('\\"', '"')
.replace('\\\\', '\\')
print("${BLUE}${stdout}${RESET}")
}
if (stderrMatch.find()) {
def stderr = stderrMatch.group(1)
.replace('\\n', '\n')
.replace('\\t', '\t')
.replace('\\"', '"')
.replace('\\\\', '\\')
System.err.print("${RED}${stderr}${RESET}")
}
return
}
if (args.serviceDumpBootstrap) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
def json = """{"command":"cat /tmp/bootstrap.sh"}"""
def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST', json, apiKey)
def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/
if (stdoutMatch.find()) {
def bootstrap = stdoutMatch.group(1)
.replace('\\n', '\n')
.replace('\\t', '\t')
.replace('\\"', '"')
.replace('\\\\', '\\')
if (args.serviceDumpFile) {
try {
new File(args.serviceDumpFile).text = bootstrap
"chmod 755 ${args.serviceDumpFile}".execute().waitFor()
println("Bootstrap saved to ${args.serviceDumpFile}")
} catch (Exception e) {
System.err.println("${RED}Error: Could not write to ${args.serviceDumpFile}: ${e.message}${RESET}")
System.exit(1)
}
} else {
print(bootstrap)
}
} else {
System.err.println("${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}")
System.exit(1)
}
return
}
if (args.serviceName) {
def json = """{"name":"${args.serviceName}""""
if (args.servicePorts) {
@ -524,15 +586,27 @@ def parseArgs(argv) {
case '--tail':
args.serviceTail = argv[++i]
break
case '--sleep':
case '--freeze':
args.serviceSleep = argv[++i]
break
case '--wake':
case '--unfreeze':
args.serviceWake = argv[++i]
break
case '--destroy':
args.serviceDestroy = argv[++i]
break
case '--execute':
args.serviceExecute = argv[++i]
break
case '--command':
args.serviceCommand = argv[++i]
break
case '--dump-bootstrap':
args.serviceDumpBootstrap = argv[++i]
break
case '--dump-file':
args.serviceDumpFile = argv[++i]
break
case '--extend':
args.keyExtend = true
break
@ -575,9 +649,13 @@ Service options:
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--sleep ID Freeze service
--wake ID Unfreeze service
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
Key options:
--extend Open browser to extend key

30
un.hs
View file

@ -145,6 +145,7 @@ data ServiceOpts = ServiceOpts
data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String
| ServiceSleep String | ServiceWake String | ServiceDestroy String
| ServiceExecute String String | ServiceDumpBootstrap String (Maybe String)
| ServiceCreate
data KeyOpts = KeyOpts
@ -187,9 +188,12 @@ parseService args = return $ parseServiceArgs args defaultServiceOpts
parseServiceArgs ("--list":rest) opts = parseServiceArgs rest opts { svcAction = ServiceList }
parseServiceArgs ("--info":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceInfo id }
parseServiceArgs ("--logs":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceLogs id }
parseServiceArgs ("--sleep":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSleep id }
parseServiceArgs ("--wake":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceWake id }
parseServiceArgs ("--freeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSleep id }
parseServiceArgs ("--unfreeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceWake id }
parseServiceArgs ("--destroy":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDestroy id }
parseServiceArgs ("--execute":id:"--command":cmd:rest) opts = parseServiceArgs rest opts { svcAction = ServiceExecute id cmd }
parseServiceArgs ("--dump-bootstrap":id:file:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id (Just file) }
parseServiceArgs ("--dump-bootstrap":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id Nothing }
parseServiceArgs ("--name":n:rest) opts = parseServiceArgs rest opts { svcName = Just n }
parseServiceArgs ("--ports":p:rest) opts = parseServiceArgs rest opts { svcPorts = Just p }
parseServiceArgs ("--type":t:rest) opts = parseServiceArgs rest opts { svcType = Just t }
@ -337,6 +341,28 @@ serviceCommand opts = do
ServiceDestroy sid -> do
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/services/" ++ sid)
putStrLn $ green ++ "Service destroyed: " ++ sid ++ reset
ServiceExecute sid cmd -> do
let json = "{\"command\":\"" ++ escapeJSON cmd ++ "\"}"
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/execute") json
unless (null stdout) $ putStr $ blue ++ stdout ++ reset
ServiceDumpBootstrap sid maybeFile -> do
hPutStrLn stderr $ "Fetching bootstrap script from " ++ sid ++ "..."
let json = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/execute") json
-- Extract stdout from JSON response
let bootstrapScript = extractJsonString stdout "stdout"
case bootstrapScript of
Just script | not (null script) -> do
case maybeFile of
Just file -> do
writeFile file script
perms <- getPermissions file
setPermissions file (setOwnerExecutable True perms)
putStrLn $ "Bootstrap saved to " ++ file
Nothing -> putStr script
_ -> do
hPutStrLn stderr $ red ++ "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" ++ reset
exitFailure
ServiceCreate -> do
case svcName opts of
Nothing -> do

38
un.jl
View file

@ -288,6 +288,34 @@ function cmd_service(args)
return
end
if args["dump-bootstrap"] !== nothing
println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...")
payload = Dict("command" => "cat /tmp/bootstrap.sh")
result = api_request("/services/$(args["dump-bootstrap"])/execute", api_key, method="POST", data=payload)
if haskey(result, "stdout") && !isempty(result["stdout"])
bootstrap = result["stdout"]
if args["dump-file"] !== nothing
# Write to file
try
write(args["dump-file"], bootstrap)
chmod(args["dump-file"], 0o755)
println("Bootstrap saved to $(args["dump-file"])")
catch e
println(stderr, "$(RED)Error: Could not write to $(args["dump-file"]): $e$(RESET)")
exit(1)
end
else
# Print to stdout
print(bootstrap)
end
else
println(stderr, "$(RED)Error: Failed to fetch bootstrap (service not running or no bootstrap file)$(RESET)")
exit(1)
end
return
end
# Create new service
if args["name"] !== nothing
payload = Dict("name" => args["name"])
@ -332,7 +360,7 @@ function cmd_service(args)
return
end
println(stderr, "$(RED)Error: Use --name to create, or --list, --info, --logs, --sleep, --wake, --destroy$(RESET)")
println(stderr, "$(RED)Error: Use --name to create, or --list, --info, --logs, --freeze, --unfreeze, --destroy$(RESET)")
exit(1)
end
@ -550,12 +578,16 @@ function main()
help = "Get service details"
"--logs"
help = "Get all logs"
"--sleep"
"--freeze"
help = "Freeze service"
"--wake"
"--unfreeze"
help = "Unfreeze service"
"--destroy"
help = "Destroy service"
"--dump-bootstrap"
help = "Dump bootstrap script from service"
"--dump-file"
help = "File to save bootstrap (with --dump-bootstrap)"
"--api-key", "-k"
help = "API key"
end

65
un.kt
View file

@ -94,6 +94,10 @@ data class Args(
var serviceSleep: String? = null,
var serviceWake: String? = null,
var serviceDestroy: String? = null,
var serviceExecute: String? = null,
var serviceCommand: String? = null,
var serviceDumpBootstrap: String? = null,
var serviceDumpFile: String? = null,
var keyExtend: Boolean = false
)
@ -301,6 +305,51 @@ fun cmdService(args: Args) {
return
}
if (args.serviceExecute != null) {
val payload = mutableMapOf<String, Any>("command" to args.serviceCommand!!)
val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, apiKey)
if (result.containsKey("stdout")) {
val stdout = result["stdout"] as? String
if (stdout != null && stdout.isNotEmpty()) {
print("$BLUE$stdout$RESET")
}
}
if (result.containsKey("stderr")) {
val stderr = result["stderr"] as? String
if (stderr != null && stderr.isNotEmpty()) {
System.err.print("$RED$stderr$RESET")
}
}
return
}
if (args.serviceDumpBootstrap != null) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
val payload = mutableMapOf<String, Any>("command" to "cat /tmp/bootstrap.sh")
val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, apiKey)
val bootstrap = result["stdout"] as? String
if (bootstrap != null && bootstrap.isNotEmpty()) {
if (args.serviceDumpFile != null) {
try {
val file = java.io.File(args.serviceDumpFile!!)
file.writeText(bootstrap)
file.setExecutable(true)
println("Bootstrap saved to ${args.serviceDumpFile}")
} catch (e: Exception) {
System.err.println("${RED}Error: Could not write to ${args.serviceDumpFile}: ${e.message}${RESET}")
exitProcess(1)
}
} else {
print(bootstrap)
}
} else {
System.err.println("${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}")
exitProcess(1)
}
return
}
if (args.serviceName != null) {
val payload = mutableMapOf<String, Any>("name" to args.serviceName!!)
if (args.servicePorts != null) {
@ -597,9 +646,13 @@ fun parseArgs(args: Array<String>): Args {
"--info" -> result.serviceInfo = args[++i]
"--logs" -> result.serviceLogs = args[++i]
"--tail" -> result.serviceTail = args[++i]
"--sleep" -> result.serviceSleep = args[++i]
"--wake" -> result.serviceWake = args[++i]
"--freeze" -> result.serviceSleep = args[++i]
"--unfreeze" -> result.serviceWake = args[++i]
"--destroy" -> result.serviceDestroy = args[++i]
"--execute" -> result.serviceExecute = args[++i]
"--command" -> result.serviceCommand = args[++i]
"--dump-bootstrap" -> result.serviceDumpBootstrap = args[++i]
"--dump-file" -> result.serviceDumpFile = args[++i]
"--extend" -> result.keyExtend = true
else -> if (!args[i].startsWith("-")) result.sourceFile = args[i]
}
@ -638,9 +691,13 @@ Service options:
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--sleep ID Freeze service
--wake ID Unfreeze service
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
Key options:
--extend Open browser to extend key

34
un.lisp
View file

@ -183,6 +183,30 @@
((string= action "destroy")
(curl-delete api-key (format nil "/services/~a" id))
(format t "~aService destroyed: ~a~a~%" *green* id *reset*))
((string= action "execute")
(when (and id bootstrap)
(let* ((json (format nil "{\"command\":\"~a\"}" (escape-json bootstrap)))
(response (curl-post api-key (format nil "/services/~a/execute" id) json))
(stdout-val (parse-json-field response "stdout")))
(when stdout-val
(format t "~a~a~a" *blue* stdout-val *reset*)))))
((string= action "dump-bootstrap")
(when id
(format *error-output* "Fetching bootstrap script from ~a...~%" id)
(let* ((json "{\"command\":\"cat /tmp/bootstrap.sh\"}")
(response (curl-post api-key (format nil "/services/~a/execute" id) json))
(stdout-val (parse-json-field response "stdout")))
(if stdout-val
(if service-type
(progn
(with-open-file (stream service-type :direction :output :if-exists :supersede)
(write-string stdout-val stream))
(uiop:run-program (list "chmod" "755" service-type))
(format t "Bootstrap saved to ~a~%" service-type))
(format t "~a" stdout-val))
(progn
(format *error-output* "~aError: Failed to fetch bootstrap (service not running or no bootstrap file)~a~%" *red* *reset*)
(uiop:quit 1))))))
((and (string= action "create") name)
(let* ((ports-json (if ports (format nil ",\"ports\":[~a]" ports) ""))
(bootstrap-json (if bootstrap (format nil ",\"bootstrap\":\"~a\"" (escape-json bootstrap)) ""))
@ -307,12 +331,18 @@
(service-cmd "info" (third args) nil nil nil nil))
((and (> (length args) 2) (string= (second args) "--logs"))
(service-cmd "logs" (third args) nil nil nil nil))
((and (> (length args) 2) (string= (second args) "--sleep"))
((and (> (length args) 2) (string= (second args) "--freeze"))
(service-cmd "sleep" (third args) nil nil nil nil))
((and (> (length args) 2) (string= (second args) "--wake"))
((and (> (length args) 2) (string= (second args) "--unfreeze"))
(service-cmd "wake" (third args) nil nil nil nil))
((and (> (length args) 2) (string= (second args) "--destroy"))
(service-cmd "destroy" (third args) nil nil nil nil))
((and (> (length args) 3) (string= (second args) "--execute"))
(service-cmd "execute" (third args) nil nil (fourth args) nil))
((and (> (length args) 3) (string= (second args) "--dump-bootstrap"))
(service-cmd "dump-bootstrap" (third args) nil nil nil (fourth args)))
((and (> (length args) 2) (string= (second args) "--dump-bootstrap"))
(service-cmd "dump-bootstrap" (third args) nil nil nil nil))
((and (> (length args) 2) (string= (second args) "--name"))
(let* ((name (third args))
(rest-args (nthcdr 3 args))

43
un.m
View file

@ -444,6 +444,8 @@ void cmdService(NSArray* args) {
NSString* sleepId = nil;
NSString* wakeId = nil;
NSString* destroyId = nil;
NSString* dumpBootstrapId = nil;
NSString* dumpFile = nil;
NSString* name = nil;
NSString* ports = nil;
NSString* type = nil;
@ -460,12 +462,16 @@ void cmdService(NSArray* args) {
infoId = args[++i];
} else if ([arg isEqualToString:@"--logs"] && i + 1 < [args count]) {
logsId = args[++i];
} else if ([arg isEqualToString:@"--sleep"] && i + 1 < [args count]) {
} else if ([arg isEqualToString:@"--freeze"] && i + 1 < [args count]) {
sleepId = args[++i];
} else if ([arg isEqualToString:@"--wake"] && i + 1 < [args count]) {
} else if ([arg isEqualToString:@"--unfreeze"] && i + 1 < [args count]) {
wakeId = args[++i];
} else if ([arg isEqualToString:@"--destroy"] && i + 1 < [args count]) {
destroyId = args[++i];
} else if ([arg isEqualToString:@"--dump-bootstrap"] && i + 1 < [args count]) {
dumpBootstrapId = args[++i];
} else if ([arg isEqualToString:@"--dump-file"] && i + 1 < [args count]) {
dumpFile = args[++i];
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
name = args[++i];
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
@ -541,6 +547,39 @@ void cmdService(NSArray* args) {
return;
}
if (dumpBootstrapId) {
fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]);
NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"};
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/execute", dumpBootstrapId];
NSDictionary* result = apiRequest(endpoint, @"POST", payload, apiKey);
if (result[@"stdout"] && [result[@"stdout"] length] > 0) {
NSString* bootstrap = result[@"stdout"];
if (dumpFile) {
// Write to file
NSError* error = nil;
[bootstrap writeToFile:dumpFile atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
fprintf(stderr, "%sError: Could not write to %s: %s%s\n",
[RED UTF8String], [dumpFile UTF8String],
[[error localizedDescription] UTF8String], [RESET UTF8String]);
exit(1);
}
NSFileManager* fm = [NSFileManager defaultManager];
[fm setAttributes:@{NSFilePosixPermissions: @0755} ofItemAtPath:dumpFile error:nil];
printf("Bootstrap saved to %s\n", [dumpFile UTF8String]);
} else {
// Print to stdout
printf("%s", [bootstrap UTF8String]);
}
} else {
fprintf(stderr, "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s\n",
[RED UTF8String], [RESET UTF8String]);
exit(1);
}
return;
}
// Create new service
if (name) {
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"name": name}];

77
un.ml
View file

@ -402,7 +402,7 @@ let service_command action name ports bootstrap service_type network vcpu =
Sys.remove tmp_file;
Printf.printf "%sService sleeping: %s%s\n" green sid reset
| None ->
Printf.fprintf stderr "Error: --sleep requires service ID\n";
Printf.fprintf stderr "Error: --freeze requires service ID\n";
exit 1)
| "wake" ->
(match name with
@ -417,7 +417,7 @@ let service_command action name ports bootstrap service_type network vcpu =
Sys.remove tmp_file;
Printf.printf "%sService waking: %s%s\n" green sid reset
| None ->
Printf.fprintf stderr "Error: --wake requires service ID\n";
Printf.fprintf stderr "Error: --unfreeze requires service ID\n";
exit 1)
| "destroy" ->
(match name with
@ -427,6 +427,72 @@ let service_command action name ports bootstrap service_type network vcpu =
| None ->
Printf.fprintf stderr "Error: --destroy requires service ID\n";
exit 1)
| "execute" ->
(match name with
| Some sid ->
(match bootstrap with
| Some cmd ->
let json = Printf.sprintf "{\"command\":\"%s\"}" (escape_json cmd) in
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
let oc = open_out tmp_file in
output_string oc json;
close_out oc;
let curl_cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com/services/%s/execute -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s"
sid api_key tmp_file in
let ic = Unix.open_process_in curl_cmd in
let rec read_all acc =
try let line = input_line ic in read_all (acc ^ line ^ "\n")
with End_of_file -> acc
in
let response = read_all "" in
let _ = Unix.close_process_in ic in
Sys.remove tmp_file;
(match extract_field "stdout" response with
| Some s -> Printf.printf "%s%s%s" blue (unescape_json s) reset
| None -> ())
| None ->
Printf.fprintf stderr "Error: --command required with --execute\n";
exit 1)
| None ->
Printf.fprintf stderr "Error: --execute requires service ID\n";
exit 1)
| "dump_bootstrap" ->
(match name with
| Some sid ->
Printf.fprintf stderr "Fetching bootstrap script from %s...\n" sid;
let json = "{\"command\":\"cat /tmp/bootstrap.sh\"}" in
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
let oc = open_out tmp_file in
output_string oc json;
close_out oc;
let curl_cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com/services/%s/execute -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s"
sid api_key tmp_file in
let ic = Unix.open_process_in curl_cmd in
let rec read_all acc =
try let line = input_line ic in read_all (acc ^ line ^ "\n")
with End_of_file -> acc
in
let response = read_all "" in
let _ = Unix.close_process_in ic in
Sys.remove tmp_file;
(match extract_field "stdout" response with
| Some s ->
let script = unescape_json s in
(match service_type with
| Some file ->
let oc = open_out file in
output_string oc script;
close_out oc;
Unix.chmod file 0o755;
Printf.printf "Bootstrap saved to %s\n" file
| None ->
Printf.printf "%s" script)
| None ->
Printf.fprintf stderr "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s\n" red reset;
exit 1)
| None ->
Printf.fprintf stderr "Error: --dump-bootstrap requires service ID\n";
exit 1)
| "create" ->
(match name with
| Some n ->
@ -488,9 +554,12 @@ let () =
| "--list" :: rest -> parse_service "list" name ports bootstrap service_type network vcpu rest
| "--info" :: id :: rest -> parse_service "info" (Some id) ports bootstrap service_type network vcpu rest
| "--logs" :: id :: rest -> parse_service "logs" (Some id) ports bootstrap service_type network vcpu rest
| "--sleep" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap service_type network vcpu rest
| "--wake" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap service_type network vcpu rest
| "--freeze" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap service_type network vcpu rest
| "--unfreeze" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap service_type network vcpu rest
| "--destroy" :: id :: rest -> parse_service "destroy" (Some id) ports bootstrap service_type network vcpu rest
| "--execute" :: id :: "--command" :: cmd :: rest -> parse_service "execute" (Some id) ports (Some cmd) service_type network vcpu rest
| "--dump-bootstrap" :: id :: file :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap (Some file) network vcpu rest
| "--dump-bootstrap" :: id :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap service_type network vcpu rest
| "--name" :: n :: rest -> parse_service "create" (Some n) ports bootstrap service_type network vcpu rest
| "--ports" :: p :: rest -> parse_service action name (Some p) bootstrap service_type network vcpu rest
| "--bootstrap" :: b :: rest -> parse_service action name ports (Some b) service_type network vcpu rest

84
un.nim
View file

@ -128,7 +128,7 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'"""
echo execCurl(cmd)
proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, network: string, vcpu: int, apiKey: string) =
proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, apiKey: string) =
if list:
let cmd = fmt"""curl -s -X GET '{API_BASE}/services' -H 'Authorization: Bearer {apiKey}'"""
echo execCurl(cmd)
@ -167,6 +167,76 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
echo GREEN & "Service destroyed: " & destroy & RESET
return
if execute != "":
let json = fmt"""{"command":"{escapeJson(command)}"}"""
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'"""
let result = execCurl(cmd)
# Simple parsing for stdout/stderr
let stdoutStart = result.find("\"stdout\":\"")
if stdoutStart >= 0:
let start = stdoutStart + 10
var endPos = start
while endPos < result.len:
if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'):
break
inc endPos
if endPos > start:
var output = result[start..<endPos]
output = output.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\")
stdout.write(output)
let stderrStart = result.find("\"stderr\":\"")
if stderrStart >= 0:
let start = stderrStart + 10
var endPos = start
while endPos < result.len:
if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'):
break
inc endPos
if endPos > start:
var errout = result[start..<endPos]
errout = errout.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\")
stderr.write(errout)
return
if dumpBootstrap != "":
stderr.writeLine("Fetching bootstrap script from " & dumpBootstrap & "...")
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{dumpBootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{{"command":"cat /tmp/bootstrap.sh"}}'"""
let result = execCurl(cmd)
let stdoutStart = result.find("\"stdout\":\"")
if stdoutStart >= 0:
let start = stdoutStart + 10
var endPos = start
while endPos < result.len:
if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'):
break
inc endPos
if endPos > start:
var bootstrapScript = result[start..<endPos]
bootstrapScript = bootstrapScript.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\")
if dumpFile != "":
try:
writeFile(dumpFile, bootstrapScript)
when defined(posix):
import os
setFilePermissions(dumpFile, {fpUserExec, fpUserWrite, fpUserRead, fpGroupExec, fpGroupRead, fpOthersExec, fpOthersRead})
echo "Bootstrap saved to " & dumpFile
except IOError as e:
stderr.writeLine(RED & "Error: Could not write to " & dumpFile & ": " & e.msg & RESET)
quit(1)
else:
stdout.write(bootstrapScript)
else:
stderr.writeLine(RED & "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" & RESET)
quit(1)
else:
stderr.writeLine(RED & "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" & RESET)
quit(1)
return
if name != "":
var json = fmt"""{"name":"{name}""""
if ports != "": json.add(fmt""","ports":[{ports}]""")
@ -309,7 +379,7 @@ proc main() =
if args[0] == "service":
var name, ports, bootstrap, serviceType = ""
var list = false
var info, logs, tail, sleep, wake, destroy, network = ""
var info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network = ""
var vcpu = 0
var i = 1
while i < args.len:
@ -322,14 +392,18 @@ proc main() =
of "--info": info = args[i+1]; inc i
of "--logs": logs = args[i+1]; inc i
of "--tail": tail = args[i+1]; inc i
of "--sleep": sleep = args[i+1]; inc i
of "--wake": wake = args[i+1]; inc i
of "--freeze": sleep = args[i+1]; inc i
of "--unfreeze": wake = args[i+1]; inc i
of "--destroy": destroy = args[i+1]; inc i
of "--execute": execute = args[i+1]; inc i
of "--command": command = args[i+1]; inc i
of "--dump-bootstrap": dumpBootstrap = args[i+1]; inc i
of "--dump-file": dumpFile = args[i+1]; inc i
of "-n": network = args[i+1]; inc i
of "-v": vcpu = parseInt(args[i+1]); inc i
of "-k": apiKey = args[i+1]; inc i
inc i
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, network, vcpu, apiKey)
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, apiKey)
return
# Execute mode

26
un.pro
View file

@ -178,6 +178,21 @@ service_destroy(ServiceId) :-
[ServiceId, ApiKey, ServiceId]),
shell(Cmd, 0).
% Service dump bootstrap
service_dump_bootstrap(ServiceId, DumpFile) :-
get_api_key(ApiKey),
( DumpFile = ''
-> % No file specified, print to stdout
format(atom(Cmd),
'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
[ServiceId, ServiceId, ApiKey])
; % File specified, save to file
format(atom(Cmd),
'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT" > "~w" && chmod 755 "~w" && echo "Bootstrap saved to ~w"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
[ServiceId, ServiceId, ApiKey, DumpFile, DumpFile, DumpFile])
),
shell(Cmd, 0).
% Service create
service_create(Name, Ports, Bootstrap, ServiceType) :-
get_api_key(ApiKey),
@ -244,16 +259,21 @@ parse_service_args([], Name, Ports, Bootstrap, ServiceType, create) :-
parse_service_args([], _, _, _, _, Action) :-
( Action = list
-> service_list
; write(user_error, 'Error: Use --list, --info, --logs, --sleep, --wake, --destroy, or --name\n'),
; write(user_error, 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --name\n'),
halt(1)
).
parse_service_args(['--list'|_], _, _, _, _, _) :- service_list.
parse_service_args(['-l'|_], _, _, _, _, _) :- service_list.
parse_service_args(['--info', ServiceId|_], _, _, _, _, _) :- service_info(ServiceId).
parse_service_args(['--logs', ServiceId|_], _, _, _, _, _) :- service_logs(ServiceId).
parse_service_args(['--sleep', ServiceId|_], _, _, _, _, _) :- service_sleep(ServiceId).
parse_service_args(['--wake', ServiceId|_], _, _, _, _, _) :- service_wake(ServiceId).
parse_service_args(['--freeze', ServiceId|_], _, _, _, _, _) :- service_sleep(ServiceId).
parse_service_args(['--unfreeze', ServiceId|_], _, _, _, _, _) :- service_wake(ServiceId).
parse_service_args(['--destroy', ServiceId|_], _, _, _, _, _) :- service_destroy(ServiceId).
parse_service_args(['--dump-bootstrap', ServiceId|Rest], _, _, _, _, _) :-
( Rest = ['--dump-file', DumpFile|_]
-> service_dump_bootstrap(ServiceId, DumpFile)
; service_dump_bootstrap(ServiceId, '')
).
parse_service_args(['--name', Name|Rest], _, Ports, Bootstrap, ServiceType, _) :-
parse_service_args(Rest, Name, Ports, Bootstrap, ServiceType, create).
parse_service_args(['--ports', PortsList|Rest], Name, _, Bootstrap, ServiceType, Action) :-

60
un.ps1
View file

@ -248,16 +248,16 @@ function Invoke-Service {
return
}
if ($Args -contains "--sleep") {
$idx = [array]::IndexOf($Args, "--sleep")
if ($Args -contains "--freeze") {
$idx = [array]::IndexOf($Args, "--freeze")
$serviceId = $Args[$idx + 1]
Invoke-Api -Endpoint "/services/$serviceId/sleep" -Method "POST" -Body "{}"
Write-Host "`e[32mService sleeping: $serviceId`e[0m"
return
}
if ($Args -contains "--wake") {
$idx = [array]::IndexOf($Args, "--wake")
if ($Args -contains "--unfreeze") {
$idx = [array]::IndexOf($Args, "--unfreeze")
$serviceId = $Args[$idx + 1]
Invoke-Api -Endpoint "/services/$serviceId/wake" -Method "POST" -Body "{}"
Write-Host "`e[32mService waking: $serviceId`e[0m"
@ -272,6 +272,36 @@ function Invoke-Service {
return
}
if ($Args -contains "--dump-bootstrap") {
$idx = [array]::IndexOf($Args, "--dump-bootstrap")
$serviceId = $Args[$idx + 1]
Write-Host "Fetching bootstrap script from $serviceId..." -ForegroundColor Yellow
$payload = @{ command = "cat /tmp/bootstrap.sh" } | ConvertTo-Json
$result = Invoke-Api -Endpoint "/services/$serviceId/execute" -Method "POST" -Body $payload
if ($result.stdout -and $result.stdout.Length -gt 0) {
$bootstrap = $result.stdout
if ($Args -contains "--dump-file") {
$dumpIdx = [array]::IndexOf($Args, "--dump-file")
$dumpFile = $Args[$dumpIdx + 1]
# Write to file
$bootstrap | Set-Content -Path $dumpFile -NoNewline
if ($IsLinux -or $IsMacOS) {
& chmod 755 $dumpFile
}
Write-Host "Bootstrap saved to $dumpFile"
} else {
# Print to stdout
Write-Host $bootstrap -NoNewline
}
} else {
Write-Error "Error: Failed to fetch bootstrap (service not running or no bootstrap file)"
exit 1
}
return
}
# Create service
if ($Args -contains "--name") {
$idx = [array]::IndexOf($Args, "--name")
@ -324,16 +354,18 @@ Session options:
--shell NAME Shell/REPL to use
Service options:
--name NAME Service name
--ports PORTS Comma-separated ports
--type TYPE Service type (minecraft, mumble, teamspeak, source, tcp, udp)
--bootstrap CMD Bootstrap command
--list, -l List services
--info ID Get service info
--logs ID Get logs
--sleep ID Freeze service
--wake ID Unfreeze service
--destroy ID Destroy service
--name NAME Service name
--ports PORTS Comma-separated ports
--type TYPE Service type (minecraft, mumble, teamspeak, source, tcp, udp)
--bootstrap CMD Bootstrap command
--list, -l List services
--info ID Get service info
--logs ID Get logs
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--dump-bootstrap ID Dump bootstrap script from service
--dump-file FILE Save bootstrap to file (with --dump-bootstrap)
Key options:
--extend Open browser to extend key

44
un.r
View file

@ -354,6 +354,34 @@ cmd_service <- function(args) {
return()
}
if (!is.null(args$dump_bootstrap)) {
cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr())
payload <- list(command = "cat /tmp/bootstrap.sh")
result <- api_request(paste0("/services/", args$dump_bootstrap, "/execute"), api_key, method = "POST", data = payload)
if (!is.null(result$stdout) && result$stdout != "") {
bootstrap <- result$stdout
if (!is.null(args$dump_file)) {
# Write to file
tryCatch({
writeLines(bootstrap, args$dump_file)
Sys.chmod(args$dump_file, mode = "0755")
cat(sprintf("Bootstrap saved to %s\n", args$dump_file))
}, error = function(e) {
cat(sprintf("%sError: Could not write to %s: %s%s\n", RED, args$dump_file, e$message, RESET), file = stderr())
quit(status = 1)
})
} else {
# Print to stdout
cat(bootstrap)
}
} else {
cat(sprintf("%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s\n", RED, RESET), file = stderr())
quit(status = 1)
}
return()
}
if (!is.null(args$name)) {
payload <- list(name = args$name)
@ -396,7 +424,7 @@ cmd_service <- function(args) {
return()
}
cat(sprintf("%sError: Use --name to create, or --list, --info, --logs, --sleep, --wake, --destroy%s\n", RED, RESET), file = stderr())
cat(sprintf("%sError: Use --name to create, or --list, --info, --logs, --freeze, --unfreeze, --destroy%s\n", RED, RESET), file = stderr())
quit(status = 1)
}
@ -419,6 +447,8 @@ parse_args <- function() {
sleep = NULL,
wake = NULL,
destroy = NULL,
dump_bootstrap = NULL,
dump_file = NULL,
name = NULL,
ports = NULL,
domains = NULL,
@ -479,11 +509,11 @@ parse_args <- function() {
i <- i + 1
result$logs <- args[i]
i <- i + 1
} else if (arg == "--sleep") {
} else if (arg == "--freeze") {
i <- i + 1
result$sleep <- args[i]
i <- i + 1
} else if (arg == "--wake") {
} else if (arg == "--unfreeze") {
i <- i + 1
result$wake <- args[i]
i <- i + 1
@ -491,6 +521,14 @@ parse_args <- function() {
i <- i + 1
result$destroy <- args[i]
i <- i + 1
} else if (arg == "--dump-bootstrap") {
i <- i + 1
result$dump_bootstrap <- args[i]
i <- i + 1
} else if (arg == "--dump-file") {
i <- i + 1
result$dump_file <- args[i]
i <- i + 1
} else if (arg == "--name") {
i <- i + 1
result$name <- args[i]

37
un.raku
View file

@ -319,6 +319,8 @@ sub cmd-service(@args) {
my $sleep-id = '';
my $wake-id = '';
my $destroy-id = '';
my $dump-bootstrap-id = '';
my $dump-file = '';
my $name = '';
my $ports = '';
my $type = '';
@ -341,11 +343,11 @@ sub cmd-service(@args) {
$i++;
$logs-id = @args[$i];
}
when '--sleep' {
when '--freeze' {
$i++;
$sleep-id = @args[$i];
}
when '--wake' {
when '--unfreeze' {
$i++;
$wake-id = @args[$i];
}
@ -353,6 +355,14 @@ sub cmd-service(@args) {
$i++;
$destroy-id = @args[$i];
}
when '--dump-bootstrap' {
$i++;
$dump-bootstrap-id = @args[$i];
}
when '--dump-file' {
$i++;
$dump-file = @args[$i];
}
when '--name' {
$i++;
$name = @args[$i];
@ -428,6 +438,29 @@ sub cmd-service(@args) {
return;
}
if $dump-bootstrap-id {
note "Fetching bootstrap script from $dump-bootstrap-id...";
my %payload = command => "cat /tmp/bootstrap.sh";
my %result = api-request("/services/$dump-bootstrap-id/execute", 'POST', %payload, :$api-key);
if %result<stdout> && %result<stdout> ne '' {
my $bootstrap = %result<stdout>;
if $dump-file {
# Write to file
$dump-file.IO.spurt($bootstrap);
run 'chmod', '755', $dump-file;
say "Bootstrap saved to $dump-file";
} else {
# Print to stdout
print $bootstrap;
}
} else {
note "{$RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file){$RESET}";
exit 1;
}
return;
}
# Create new service
if $name {
my %payload = name => $name;

59
un.rs
View file

@ -348,6 +348,10 @@ fn cmd_service(
sleep: Option<&str>,
wake: Option<&str>,
destroy: Option<&str>,
execute: Option<&str>,
command: Option<&str>,
dump_bootstrap: Option<&str>,
dump_file: Option<&str>,
network: Option<&str>,
vcpu: Option<i32>,
api_key: &str,
@ -394,6 +398,53 @@ fn cmd_service(
return;
}
if let Some(id) = execute {
let cmd = command.unwrap_or("");
let json = format!(r#"{{"command":"{}"}}"#, escape_json(cmd));
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(&json), api_key);
let stdout_str = extract_json_string(&result, "stdout");
let stderr_str = extract_json_string(&result, "stderr");
if !stdout_str.is_empty() {
print!("{}{}{}", BLUE, stdout_str, RESET);
}
if !stderr_str.is_empty() {
eprint!("{}{}{}", RED, stderr_str, RESET);
}
return;
}
if let Some(id) = dump_bootstrap {
eprintln!("Fetching bootstrap script from {}...", id);
let json = r#"{"command":"cat /tmp/bootstrap.sh"}"#;
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(json), api_key);
let bootstrap = extract_json_string(&result, "stdout");
if !bootstrap.is_empty() {
if let Some(file) = dump_file {
match fs::write(file, &bootstrap) {
Ok(_) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(file, fs::Permissions::from_mode(0o755));
}
println!("Bootstrap saved to {}", file);
}
Err(e) => {
eprintln!("{}Error: Could not write to {}: {}{}", RED, file, e, RESET);
process::exit(1);
}
}
} else {
print!("{}", bootstrap);
}
} else {
eprintln!("{}Error: Failed to fetch bootstrap (service not running or no bootstrap file){}", RED, RESET);
process::exit(1);
}
return;
}
// Create service
if let Some(n) = name {
let mut json = format!(r#"{{"name":"{}""#, n);
@ -621,9 +672,13 @@ fn main() {
args.iter().position(|x| x == "--info").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--logs").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--tail").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--sleep").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--wake").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--freeze").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--unfreeze").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--destroy").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--execute").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--command").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--dump-bootstrap").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--dump-file").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
network.as_deref(),
vcpu,
&key,

34
un.scm
View file

@ -279,6 +279,30 @@
((equal? action "destroy")
(curl-delete api-key (format #f "/services/~a" id))
(format #t "~aService destroyed: ~a~a\n" green id reset))
((equal? action "execute")
(when (and id bootstrap)
(let* ((json (format #f "{\"command\":\"~a\"}" (escape-json bootstrap)))
(response (curl-post api-key (format #f "/services/~a/execute" id) json))
(stdout-val (json-extract-string response "stdout")))
(when stdout-val
(display (format #f "~a~a~a" blue stdout-val reset))))))
((equal? action "dump-bootstrap")
(when id
(format (current-error-port) "Fetching bootstrap script from ~a...\n" id)
(let* ((json "{\"command\":\"cat /tmp/bootstrap.sh\"}")
(response (curl-post api-key (format #f "/services/~a/execute" id) json))
(stdout-val (json-extract-string response "stdout")))
(if stdout-val
(if type
(begin
(call-with-output-file type
(lambda (port) (display stdout-val port)))
(system (format #f "chmod 755 ~a" type))
(format #t "Bootstrap saved to ~a\n" type))
(display stdout-val))
(begin
(format (current-error-port) "~aError: Failed to fetch bootstrap (service not running or no bootstrap file)~a\n" red reset)
(exit 1))))))
((and (equal? action "create") name)
(let* ((ports-json (if ports (format #f ",\"ports\":[~a]" ports) ""))
(bootstrap-json (if bootstrap (format #f ",\"bootstrap\":\"~a\"" (escape-json bootstrap)) ""))
@ -318,12 +342,18 @@
(service-cmd "info" (caddr args) #f #f #f #f))
((and (> (length args) 2) (equal? (cadr args) "--logs"))
(service-cmd "logs" (caddr args) #f #f #f #f))
((and (> (length args) 2) (equal? (cadr args) "--sleep"))
((and (> (length args) 2) (equal? (cadr args) "--freeze"))
(service-cmd "sleep" (caddr args) #f #f #f #f))
((and (> (length args) 2) (equal? (cadr args) "--wake"))
((and (> (length args) 2) (equal? (cadr args) "--unfreeze"))
(service-cmd "wake" (caddr args) #f #f #f #f))
((and (> (length args) 2) (equal? (cadr args) "--destroy"))
(service-cmd "destroy" (caddr args) #f #f #f #f))
((and (> (length args) 3) (equal? (cadr args) "--execute"))
(service-cmd "execute" (caddr args) #f #f (list-ref args 3) #f))
((and (> (length args) 3) (equal? (cadr args) "--dump-bootstrap"))
(service-cmd "dump-bootstrap" (caddr args) #f #f #f (list-ref args 3)))
((and (> (length args) 2) (equal? (cadr args) "--dump-bootstrap"))
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f))
((and (> (length args) 2) (equal? (cadr args) "--name"))
(let ((name (caddr args))
(ports (if (and (> (length args) 4) (equal? (list-ref args 3) "--ports"))

39
un.tcl
View file

@ -445,6 +445,8 @@ proc cmd_service {args} {
set sleep_id ""
set wake_id ""
set destroy_id ""
set dump_bootstrap_id ""
set dump_file ""
set name ""
set ports ""
set service_type ""
@ -467,11 +469,11 @@ proc cmd_service {args} {
incr i
set logs_id [lindex $args $i]
}
--sleep {
--freeze {
incr i
set sleep_id [lindex $args $i]
}
--wake {
--unfreeze {
incr i
set wake_id [lindex $args $i]
}
@ -479,6 +481,14 @@ proc cmd_service {args} {
incr i
set destroy_id [lindex $args $i]
}
--dump-bootstrap {
incr i
set dump_bootstrap_id [lindex $args $i]
}
--dump-file {
incr i
set dump_file [lindex $args $i]
}
--name {
incr i
set name [lindex $args $i]
@ -557,6 +567,31 @@ proc cmd_service {args} {
return
}
if {$dump_bootstrap_id ne ""} {
puts stderr "Fetching bootstrap script from $dump_bootstrap_id..."
set payload [list command [::json::write string "cat /tmp/bootstrap.sh"]]
set result [api_request "/services/$dump_bootstrap_id/execute" "POST" $payload $api_key]
if {[dict exists $result stdout] && [dict get $result stdout] ne ""} {
set bootstrap [dict get $result stdout]
if {$dump_file ne ""} {
# Write to file
set fp [open $dump_file w]
puts -nonewline $fp $bootstrap
close $fp
file attributes $dump_file -permissions 0755
puts "Bootstrap saved to $dump_file"
} else {
# Print to stdout
puts -nonewline $bootstrap
}
} else {
puts stderr "${::RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${::RESET}"
exit 1
}
return
}
# Create new service
if {$name ne ""} {
set payload [list name [::json::write string $name]]

68
un.v
View file

@ -268,7 +268,7 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
println(exec_curl(cmd))
}
fn cmd_service(name string, ports string, service_type string, bootstrap string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, network string, vcpu int, api_key string) {
fn cmd_service(name string, ports string, service_type string, bootstrap string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, api_key string) {
if list {
cmd := "curl -s -X GET '${api_base}/services' -H 'Authorization: Bearer ${api_key}'"
println(exec_curl(cmd))
@ -314,6 +314,46 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
return
}
if execute != '' {
json := '{"command":"${escape_json(command)}"}'
cmd := "curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'"
result := exec_curl(cmd)
stdout_str := extract_json_string(result, 'stdout')
stderr_str := extract_json_string(result, 'stderr')
if stdout_str != '' {
print(stdout_str)
}
if stderr_str != '' {
eprint(stderr_str)
}
return
}
if dump_bootstrap != '' {
eprintln('Fetching bootstrap script from ${dump_bootstrap}...')
cmd := "curl -s -X POST '${api_base}/services/${dump_bootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"
result := exec_curl(cmd)
bootstrap_script := extract_json_string(result, 'stdout')
if bootstrap_script != '' {
if dump_file != '' {
os.write_file(dump_file, bootstrap_script) or {
eprintln('${red}Error: Could not write to ${dump_file}: ${err}${reset}')
exit(1)
}
os.chmod(dump_file, 0o755) or {}
println('Bootstrap saved to ${dump_file}')
} else {
print(bootstrap_script)
}
} else {
eprintln('${red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${reset}')
exit(1)
}
return
}
if name != '' {
mut json := '{"name":"${name}"'
if ports != '' {
@ -415,6 +455,10 @@ fn main() {
mut sleep := ''
mut wake := ''
mut destroy := ''
mut execute := ''
mut command := ''
mut dump_bootstrap := ''
mut dump_file := ''
mut network := ''
mut vcpu := 0
@ -450,11 +494,11 @@ fn main() {
i++
tail = os.args[i]
}
'--sleep' {
'--freeze' {
i++
sleep = os.args[i]
}
'--wake' {
'--unfreeze' {
i++
wake = os.args[i]
}
@ -462,6 +506,22 @@ fn main() {
i++
destroy = os.args[i]
}
'--execute' {
i++
execute = os.args[i]
}
'--command' {
i++
command = os.args[i]
}
'--dump-bootstrap' {
i++
dump_bootstrap = os.args[i]
}
'--dump-file' {
i++
dump_file = os.args[i]
}
'-n' {
i++
network = os.args[i]
@ -479,7 +539,7 @@ fn main() {
i++
}
cmd_service(name, ports, service_type, bootstrap, list, info, logs, tail, sleep, wake, destroy, network,
cmd_service(name, ports, service_type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network,
vcpu, api_key)
return
}

67
un.zig
View file

@ -121,6 +121,10 @@ pub fn main() !u8 {
var ports: ?[]const u8 = null;
var service_type: ?[]const u8 = null;
var info: ?[]const u8 = null;
var execute: ?[]const u8 = null;
var command: ?[]const u8 = null;
var dump_bootstrap: ?[]const u8 = null;
var dump_file: ?[]const u8 = null;
var i: usize = 2;
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--list")) {
@ -137,6 +141,18 @@ pub fn main() !u8 {
} else if (mem.eql(u8, args[i], "--info") and i + 1 < args.len) {
i += 1;
info = args[i];
} else if (mem.eql(u8, args[i], "--execute") and i + 1 < args.len) {
i += 1;
execute = args[i];
} else if (mem.eql(u8, args[i], "--command") and i + 1 < args.len) {
i += 1;
command = args[i];
} else if (mem.eql(u8, args[i], "--dump-bootstrap") and i + 1 < args.len) {
i += 1;
dump_bootstrap = args[i];
} else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) {
i += 1;
dump_file = args[i];
}
}
@ -150,6 +166,57 @@ pub fn main() !u8 {
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (execute) |exec_id| {
const cmd_text = command orelse "";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"{s}\"}}'", .{ API_BASE, exec_id, api_key, cmd_text });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (dump_bootstrap) |bootstrap_id| {
std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id});
const tmp_file = "/tmp/unsandbox_bootstrap_dump.txt";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"cat /tmp/bootstrap.sh\"}}' -o {s}", .{ API_BASE, bootstrap_id, api_key, tmp_file });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Read the JSON response
const json_content = fs.cwd().readFileAlloc(allocator, tmp_file, 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading response: {}\x1b[0m\n", .{err});
std.fs.cwd().deleteFile(tmp_file) catch {};
return 1;
};
defer allocator.free(json_content);
std.fs.cwd().deleteFile(tmp_file) catch {};
// Extract stdout from JSON (simple string search)
const stdout_prefix = "\"stdout\":\"";
if (mem.indexOf(u8, json_content, stdout_prefix)) |start_idx| {
const value_start = start_idx + stdout_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
const bootstrap_content = json_content[value_start..end_idx];
if (dump_file) |file_path| {
const file = try std.fs.cwd().createFile(file_path, .{});
defer file.close();
try file.writeAll(bootstrap_content);
// Set permissions (Unix only)
if (@import("builtin").os.tag != .windows) {
const chmod_cmd = try std.fmt.allocPrint(allocator, "chmod 755 {s}", .{file_path});
defer allocator.free(chmod_cmd);
_ = std.c.system(chmod_cmd.ptr);
}
std.debug.print("Bootstrap saved to {s}\n", .{file_path});
} else {
std.debug.print("{s}", .{bootstrap_content});
}
} else {
std.debug.print("\x1b[31mError: Failed to parse bootstrap response\x1b[0m\n", .{});
return 1;
}
} else {
std.debug.print("\x1b[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\x1b[0m\n", .{});
return 1;
}
} else if (name) |n| {
var json_buf: [4096]u8 = undefined;
var json_stream = std.io.fixedBufferStream(&json_buf);

View file

@ -426,12 +426,12 @@ async function cmdService(args: string[]) {
logsId = args[++i];
}
break;
case "--sleep":
case "--freeze":
if (i + 1 < args.length) {
sleepId = args[++i];
}
break;
case "--wake":
case "--unfreeze":
if (i + 1 < args.length) {
wakeId = args[++i];
}

View file

@ -640,8 +640,8 @@ int main(int argc, char *argv[]) {
else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) info = argv[++i];
else if (strcmp(argv[i], "--logs") == 0 && i + 1 < argc) logs = argv[++i];
else if (strcmp(argv[i], "--tail") == 0 && i + 1 < argc) tail = argv[++i];
else if (strcmp(argv[i], "--sleep") == 0 && i + 1 < argc) sleep_svc = argv[++i];
else if (strcmp(argv[i], "--wake") == 0 && i + 1 < argc) wake = argv[++i];
else if (strcmp(argv[i], "--freeze") == 0 && i + 1 < argc) sleep_svc = argv[++i];
else if (strcmp(argv[i], "--unfreeze") == 0 && i + 1 < argc) wake = argv[++i];
else if (strcmp(argv[i], "--destroy") == 0 && i + 1 < argc) destroy = argv[++i];
else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) network = argv[++i];
else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]);