refactor: consolidate dotnet implementations and add full CLI spec
- Remove misplaced clients/csharp/dotnet/ (was duplicate in wrong location) - Add clients/dotnet/async/ with async/await implementation (1111 lines) - Update clients/dotnet/sync/ with blocking HttpClient.Send (1096 lines) - Both versions now implement complete CLI spec: - execute, session, service, service env, key (existing) - snapshot command (list, info, delete, lock, unlock, clone) - image command (list, info, delete, lock, unlock, publish, visibility, spawn, clone) - languages command with 1-hour cache - Added session options: freeze, unfreeze, boost, unboost, snapshot - Added service options: lock, unlock, resize, redeploy, snapshot, bootstrap-file
This commit is contained in:
parent
c3589552d2
commit
fe980a5dad
4 changed files with 1599 additions and 1073 deletions
File diff suppressed because it is too large
Load diff
1111
clients/dotnet/async/src/Un.cs
Normal file
1111
clients/dotnet/async/src/Un.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,10 +3,10 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<RootNamespace>Unsandbox</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>un</AssemblyName>
|
<AssemblyName>un</AssemblyName>
|
||||||
<RootNamespace>Unsandbox.Cli</RootNamespace>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||||
//
|
//
|
||||||
// Un.cs - Unsandbox CLI Client (.NET 10 Implementation)
|
// Un.cs - Unsandbox CLI Client (.NET 10 Synchronous Implementation)
|
||||||
// Build: dotnet build
|
// Build: dotnet build
|
||||||
// Run: dotnet run -- [options] <source_file>
|
// Run: dotnet run -- [options] <source_file>
|
||||||
// Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
// Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
||||||
|
|
@ -59,18 +59,23 @@ try
|
||||||
|
|
||||||
if (parsedArgs.ShowVersion)
|
if (parsedArgs.ShowVersion)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"un {VERSION} (.NET 10)");
|
Console.WriteLine($"un {VERSION} (.NET 10 sync)");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await (parsedArgs.Command switch
|
switch (parsedArgs.Command)
|
||||||
{
|
{
|
||||||
"session" => CmdSessionAsync(parsedArgs),
|
case "session": CmdSession(parsedArgs); break;
|
||||||
"service" => CmdServiceAsync(parsedArgs),
|
case "service": CmdService(parsedArgs); break;
|
||||||
"key" => CmdKeyAsync(parsedArgs),
|
case "snapshot": CmdSnapshot(parsedArgs); break;
|
||||||
_ when parsedArgs.SourceFile != null => CmdExecuteAsync(parsedArgs),
|
case "image": CmdImage(parsedArgs); break;
|
||||||
_ => Task.Run(() => { PrintHelp(); Environment.Exit(1); })
|
case "languages": CmdLanguages(parsedArgs); break;
|
||||||
});
|
case "key": CmdKey(parsedArgs); break;
|
||||||
|
default:
|
||||||
|
if (parsedArgs.SourceFile != null) CmdExecute(parsedArgs);
|
||||||
|
else { PrintHelp(); return 1; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -80,10 +85,10 @@ catch (Exception ex)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task CmdExecuteAsync(Args args)
|
void CmdExecute(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
var code = await File.ReadAllTextAsync(args.SourceFile!);
|
var code = File.ReadAllText(args.SourceFile!);
|
||||||
var language = DetectLanguage(args.SourceFile!);
|
var language = DetectLanguage(args.SourceFile!);
|
||||||
|
|
||||||
var payload = new Dictionary<string, object> { ["language"] = language, ["code"] = code };
|
var payload = new Dictionary<string, object> { ["language"] = language, ["code"] = code };
|
||||||
|
|
@ -102,7 +107,7 @@ async Task CmdExecuteAsync(Args args)
|
||||||
var inputFiles = new List<Dictionary<string, string>>();
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
foreach (var filepath in args.Files)
|
foreach (var filepath in args.Files)
|
||||||
{
|
{
|
||||||
var content = await File.ReadAllBytesAsync(filepath);
|
var content = File.ReadAllBytes(filepath);
|
||||||
inputFiles.Add(new Dictionary<string, string>
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["filename"] = Path.GetFileName(filepath),
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
|
@ -116,7 +121,7 @@ async Task CmdExecuteAsync(Args args)
|
||||||
if (args.Network != null) payload["network"] = args.Network;
|
if (args.Network != null) payload["network"] = args.Network;
|
||||||
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
||||||
|
|
||||||
var result = await ApiRequestAsync("/execute", HttpMethod.Post, payload, publicKey, secretKey);
|
var result = ApiRequest("/execute", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
|
||||||
if (result.TryGetValue("stdout", out var stdout) && stdout is JsonElement stdoutEl)
|
if (result.TryGetValue("stdout", out var stdout) && stdout is JsonElement stdoutEl)
|
||||||
Console.Write($"{BLUE}{stdoutEl.GetString()}{RESET}");
|
Console.Write($"{BLUE}{stdoutEl.GetString()}{RESET}");
|
||||||
|
|
@ -132,7 +137,7 @@ async Task CmdExecuteAsync(Args args)
|
||||||
var filename = artifact.GetProperty("filename").GetString() ?? "artifact";
|
var filename = artifact.GetProperty("filename").GetString() ?? "artifact";
|
||||||
var contentB64 = artifact.GetProperty("content_base64").GetString() ?? "";
|
var contentB64 = artifact.GetProperty("content_base64").GetString() ?? "";
|
||||||
var path = Path.Combine(outDir, filename);
|
var path = Path.Combine(outDir, filename);
|
||||||
await File.WriteAllBytesAsync(path, Convert.FromBase64String(contentB64));
|
File.WriteAllBytes(path, Convert.FromBase64String(contentB64));
|
||||||
Console.Error.WriteLine($"{GREEN}Saved: {path}{RESET}");
|
Console.Error.WriteLine($"{GREEN}Saved: {path}{RESET}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -141,48 +146,86 @@ async Task CmdExecuteAsync(Args args)
|
||||||
Environment.Exit(exitCode);
|
Environment.Exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task CmdSessionAsync(Args args)
|
void CmdSession(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
|
|
||||||
if (args.SessionList)
|
if (args.SessionList)
|
||||||
{
|
{
|
||||||
var result = await ApiRequestAsync("/sessions", HttpMethod.Get, null, publicKey, secretKey);
|
var result = ApiRequest("/sessions", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
if (result.TryGetValue("sessions", out var sessions) && sessions is JsonElement sessionsEl)
|
if (result.TryGetValue("sessions", out var sessions) && sessions is JsonElement sessionsEl)
|
||||||
{
|
{
|
||||||
var sessionList = sessionsEl.EnumerateArray().ToList();
|
var sessionList = sessionsEl.EnumerateArray().ToList();
|
||||||
if (sessionList.Count == 0) { Console.WriteLine("No active sessions"); return; }
|
if (sessionList.Count == 0) { Console.WriteLine("No active sessions"); return; }
|
||||||
Console.WriteLine($"{"ID",-40} {"Shell",-10} {"Status",-10} {"Created"}");
|
Console.WriteLine($"{"ID",-40} {"Shell",-10} {"Status",-10} {"Created"}");
|
||||||
foreach (var s in sessionList)
|
foreach (var s in sessionList)
|
||||||
{
|
|
||||||
Console.WriteLine($"{GetStr(s, "id"),-40} {GetStr(s, "shell"),-10} {GetStr(s, "status"),-10} {GetStr(s, "created_at")}");
|
Console.WriteLine($"{GetStr(s, "id"),-40} {GetStr(s, "shell"),-10} {GetStr(s, "status"),-10} {GetStr(s, "created_at")}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.SessionKill != null)
|
if (args.SessionKill != null)
|
||||||
{
|
{
|
||||||
await ApiRequestAsync($"/sessions/{args.SessionKill}", HttpMethod.Delete, null, publicKey, secretKey);
|
ApiRequest($"/sessions/{args.SessionKill}", HttpMethod.Delete, null, publicKey, secretKey);
|
||||||
Console.WriteLine($"{GREEN}Session terminated: {args.SessionKill}{RESET}");
|
Console.WriteLine($"{GREEN}Session terminated: {args.SessionKill}{RESET}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = new Dictionary<string, object> { ["shell"] = args.SessionShell ?? "bash" };
|
if (args.SessionFreeze != null)
|
||||||
if (args.Network != null) payload["network"] = args.Network;
|
{
|
||||||
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
ApiRequest($"/sessions/{args.SessionFreeze}/freeze", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Session frozen: {args.SessionFreeze}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SessionUnfreeze != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/sessions/{args.SessionUnfreeze}/unfreeze", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Session unfreezing: {args.SessionUnfreeze}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SessionBoost != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/sessions/{args.SessionBoost}/boost", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Session boosted: {args.SessionBoost}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SessionUnboost != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/sessions/{args.SessionUnboost}/unboost", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Session unboosted: {args.SessionUnboost}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SessionSnapshot != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.SnapshotName != null) payload["name"] = args.SnapshotName;
|
||||||
|
if (args.SnapshotHot) payload["hot"] = true;
|
||||||
|
|
||||||
|
var result = ApiRequest($"/sessions/{args.SessionSnapshot}/snapshot", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||||
|
Console.WriteLine($"{GREEN}Snapshot created: {id}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createPayload = new Dictionary<string, object> { ["shell"] = args.SessionShell ?? "bash" };
|
||||||
|
if (args.Network != null) createPayload["network"] = args.Network;
|
||||||
|
if (args.Vcpu > 0) createPayload["vcpu"] = args.Vcpu;
|
||||||
|
|
||||||
Console.WriteLine($"{YELLOW}Creating session...{RESET}");
|
Console.WriteLine($"{YELLOW}Creating session...{RESET}");
|
||||||
var createResult = await ApiRequestAsync("/sessions", HttpMethod.Post, payload, publicKey, secretKey);
|
var createResult = ApiRequest("/sessions", HttpMethod.Post, createPayload, publicKey, secretKey);
|
||||||
var sessionId = createResult.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : "unknown";
|
var sessionId = createResult.TryGetValue("id", out var sid) && sid is JsonElement sidEl ? sidEl.GetString() : "unknown";
|
||||||
Console.WriteLine($"{GREEN}Session created: {sessionId}{RESET}");
|
Console.WriteLine($"{GREEN}Session created: {sessionId}{RESET}");
|
||||||
Console.WriteLine($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}");
|
Console.WriteLine($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}");
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task CmdKeyAsync(Args args)
|
void CmdKey(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
var result = await ApiRequestAsync("/keys/validate", HttpMethod.Post, null, publicKey, secretKey);
|
var result = ApiRequest("/keys/validate", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
|
||||||
if (!result.TryGetValue("valid", out var validObj) || validObj is not JsonElement validEl)
|
if (!result.TryGetValue("valid", out var validObj) || validObj is not JsonElement validEl)
|
||||||
{
|
{
|
||||||
|
|
@ -236,19 +279,19 @@ void OpenBrowser(string url)
|
||||||
catch (Exception ex) { Console.Error.WriteLine($"{RED}Failed to open browser: {ex.Message}{RESET}"); }
|
catch (Exception ex) { Console.Error.WriteLine($"{RED}Failed to open browser: {ex.Message}{RESET}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task CmdServiceAsync(Args args)
|
void CmdService(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(args.EnvAction))
|
if (!string.IsNullOrEmpty(args.EnvAction))
|
||||||
{
|
{
|
||||||
await CmdServiceEnvAsync(args, publicKey, secretKey);
|
CmdServiceEnv(args, publicKey, secretKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServiceList)
|
if (args.ServiceList)
|
||||||
{
|
{
|
||||||
var result = await ApiRequestAsync("/services", HttpMethod.Get, null, publicKey, secretKey);
|
var result = ApiRequest("/services", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
if (result.TryGetValue("services", out var services) && services is JsonElement servicesEl)
|
if (result.TryGetValue("services", out var services) && services is JsonElement servicesEl)
|
||||||
{
|
{
|
||||||
var serviceList = servicesEl.EnumerateArray().ToList();
|
var serviceList = servicesEl.EnumerateArray().ToList();
|
||||||
|
|
@ -266,35 +309,35 @@ async Task CmdServiceAsync(Args args)
|
||||||
|
|
||||||
if (args.ServiceInfo != null)
|
if (args.ServiceInfo != null)
|
||||||
{
|
{
|
||||||
var result = await ApiRequestAsync($"/services/{args.ServiceInfo}", HttpMethod.Get, null, publicKey, secretKey);
|
var result = ApiRequest($"/services/{args.ServiceInfo}", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServiceLogs != null)
|
if (args.ServiceLogs != null)
|
||||||
{
|
{
|
||||||
var result = await ApiRequestAsync($"/services/{args.ServiceLogs}/logs", HttpMethod.Get, null, publicKey, secretKey);
|
var result = ApiRequest($"/services/{args.ServiceLogs}/logs", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
if (result.TryGetValue("logs", out var logs) && logs is JsonElement logsEl) Console.WriteLine(logsEl.GetString());
|
if (result.TryGetValue("logs", out var logs) && logs is JsonElement logsEl) Console.WriteLine(logsEl.GetString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServiceTail != null)
|
if (args.ServiceTail != null)
|
||||||
{
|
{
|
||||||
var result = await ApiRequestAsync($"/services/{args.ServiceTail}/logs?lines=9000", HttpMethod.Get, null, publicKey, secretKey);
|
var result = ApiRequest($"/services/{args.ServiceTail}/logs?lines=9000", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
if (result.TryGetValue("logs", out var logs) && logs is JsonElement logsEl) Console.WriteLine(logsEl.GetString());
|
if (result.TryGetValue("logs", out var logs) && logs is JsonElement logsEl) Console.WriteLine(logsEl.GetString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServiceSleep != null)
|
if (args.ServiceSleep != null)
|
||||||
{
|
{
|
||||||
await ApiRequestAsync($"/services/{args.ServiceSleep}/freeze", HttpMethod.Post, null, publicKey, secretKey);
|
ApiRequest($"/services/{args.ServiceSleep}/freeze", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
Console.WriteLine($"{GREEN}Service frozen: {args.ServiceSleep}{RESET}");
|
Console.WriteLine($"{GREEN}Service frozen: {args.ServiceSleep}{RESET}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServiceWake != null)
|
if (args.ServiceWake != null)
|
||||||
{
|
{
|
||||||
await ApiRequestAsync($"/services/{args.ServiceWake}/unfreeze", HttpMethod.Post, null, publicKey, secretKey);
|
ApiRequest($"/services/{args.ServiceWake}/unfreeze", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
Console.WriteLine($"{GREEN}Service unfreezing: {args.ServiceWake}{RESET}");
|
Console.WriteLine($"{GREEN}Service unfreezing: {args.ServiceWake}{RESET}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +345,7 @@ async Task CmdServiceAsync(Args args)
|
||||||
if (args.ServiceUnfreezeOnDemand != null)
|
if (args.ServiceUnfreezeOnDemand != null)
|
||||||
{
|
{
|
||||||
var payload = new Dictionary<string, object> { ["unfreeze_on_demand"] = args.ServiceUnfreezeOnDemandEnabled };
|
var payload = new Dictionary<string, object> { ["unfreeze_on_demand"] = args.ServiceUnfreezeOnDemandEnabled };
|
||||||
await ApiRequestAsync($"/services/{args.ServiceUnfreezeOnDemand}", new HttpMethod("PATCH"), payload, publicKey, secretKey);
|
ApiRequest($"/services/{args.ServiceUnfreezeOnDemand}", new HttpMethod("PATCH"), payload, publicKey, secretKey);
|
||||||
string status = args.ServiceUnfreezeOnDemandEnabled ? "enabled" : "disabled";
|
string status = args.ServiceUnfreezeOnDemandEnabled ? "enabled" : "disabled";
|
||||||
Console.WriteLine($"{GREEN}Unfreeze-on-demand {status} for service: {args.ServiceUnfreezeOnDemand}{RESET}");
|
Console.WriteLine($"{GREEN}Unfreeze-on-demand {status} for service: {args.ServiceUnfreezeOnDemand}{RESET}");
|
||||||
return;
|
return;
|
||||||
|
|
@ -310,15 +353,57 @@ async Task CmdServiceAsync(Args args)
|
||||||
|
|
||||||
if (args.ServiceDestroy != null)
|
if (args.ServiceDestroy != null)
|
||||||
{
|
{
|
||||||
await ApiRequestAsync($"/services/{args.ServiceDestroy}", HttpMethod.Delete, null, publicKey, secretKey);
|
ApiRequest($"/services/{args.ServiceDestroy}", HttpMethod.Delete, null, publicKey, secretKey);
|
||||||
Console.WriteLine($"{GREEN}Service destroyed: {args.ServiceDestroy}{RESET}");
|
Console.WriteLine($"{GREEN}Service destroyed: {args.ServiceDestroy}{RESET}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.ServiceLock != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/services/{args.ServiceLock}/lock", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Service locked: {args.ServiceLock}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServiceUnlock != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/services/{args.ServiceUnlock}/unlock", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Service unlocked: {args.ServiceUnlock}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServiceResize != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
||||||
|
ApiRequest($"/services/{args.ServiceResize}/resize", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Service resized: {args.ServiceResize}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServiceRedeploy != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServiceSnapshot != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.SnapshotName != null) payload["name"] = args.SnapshotName;
|
||||||
|
if (args.SnapshotHot) payload["hot"] = true;
|
||||||
|
|
||||||
|
var result = ApiRequest($"/services/{args.ServiceSnapshot}/snapshot", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||||
|
Console.WriteLine($"{GREEN}Snapshot created: {id}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.ServiceExecute != null)
|
if (args.ServiceExecute != null)
|
||||||
{
|
{
|
||||||
var payload = new Dictionary<string, object> { ["command"] = args.ServiceCommand ?? "" };
|
var payload = new Dictionary<string, object> { ["command"] = args.ServiceCommand ?? "" };
|
||||||
var result = await ApiRequestAsync($"/services/{args.ServiceExecute}/execute", HttpMethod.Post, payload, publicKey, secretKey);
|
var result = ApiRequest($"/services/{args.ServiceExecute}/execute", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
if (result.TryGetValue("stdout", out var stdout) && stdout is JsonElement stdoutEl) Console.Write($"{BLUE}{stdoutEl.GetString()}{RESET}");
|
if (result.TryGetValue("stdout", out var stdout) && stdout is JsonElement stdoutEl) Console.Write($"{BLUE}{stdoutEl.GetString()}{RESET}");
|
||||||
if (result.TryGetValue("stderr", out var stderr) && stderr is JsonElement stderrEl) Console.Error.Write($"{RED}{stderrEl.GetString()}{RESET}");
|
if (result.TryGetValue("stderr", out var stderr) && stderr is JsonElement stderrEl) Console.Error.Write($"{RED}{stderrEl.GetString()}{RESET}");
|
||||||
return;
|
return;
|
||||||
|
|
@ -328,13 +413,13 @@ async Task CmdServiceAsync(Args args)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Fetching bootstrap script from {args.ServiceDumpBootstrap}...");
|
Console.Error.WriteLine($"Fetching bootstrap script from {args.ServiceDumpBootstrap}...");
|
||||||
var payload = new Dictionary<string, object> { ["command"] = "cat /tmp/bootstrap.sh" };
|
var payload = new Dictionary<string, object> { ["command"] = "cat /tmp/bootstrap.sh" };
|
||||||
var result = await ApiRequestAsync($"/services/{args.ServiceDumpBootstrap}/execute", HttpMethod.Post, payload, publicKey, secretKey);
|
var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
var bootstrap = result.TryGetValue("stdout", out var bs) && bs is JsonElement bsEl ? bsEl.GetString() : null;
|
var bootstrap = result.TryGetValue("stdout", out var bs) && bs is JsonElement bsEl ? bsEl.GetString() : null;
|
||||||
if (!string.IsNullOrEmpty(bootstrap))
|
if (!string.IsNullOrEmpty(bootstrap))
|
||||||
{
|
{
|
||||||
if (args.ServiceDumpFile != null)
|
if (args.ServiceDumpFile != null)
|
||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(args.ServiceDumpFile, bootstrap);
|
File.WriteAllText(args.ServiceDumpFile, bootstrap);
|
||||||
Console.WriteLine($"Bootstrap saved to {args.ServiceDumpFile}");
|
Console.WriteLine($"Bootstrap saved to {args.ServiceDumpFile}");
|
||||||
}
|
}
|
||||||
else Console.Write(bootstrap);
|
else Console.Write(bootstrap);
|
||||||
|
|
@ -354,11 +439,12 @@ async Task CmdServiceAsync(Args args)
|
||||||
payload["ports"] = args.ServicePorts.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
payload["ports"] = args.ServicePorts.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
||||||
if (args.ServiceType != null) payload["service_type"] = args.ServiceType;
|
if (args.ServiceType != null) payload["service_type"] = args.ServiceType;
|
||||||
if (args.ServiceBootstrap != null) payload["bootstrap"] = args.ServiceBootstrap;
|
if (args.ServiceBootstrap != null) payload["bootstrap"] = args.ServiceBootstrap;
|
||||||
|
else if (args.ServiceBootstrapFile != null) payload["bootstrap"] = File.ReadAllText(args.ServiceBootstrapFile);
|
||||||
if (args.Network != null) payload["network"] = args.Network;
|
if (args.Network != null) payload["network"] = args.Network;
|
||||||
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
||||||
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
|
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
|
||||||
|
|
||||||
var result = await ApiRequestAsync("/services", HttpMethod.Post, payload, publicKey, secretKey);
|
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
||||||
Console.WriteLine($"{GREEN}Service created: {serviceId}{RESET}");
|
Console.WriteLine($"{GREEN}Service created: {serviceId}{RESET}");
|
||||||
if (result.TryGetValue("name", out var name) && name is JsonElement nameEl) Console.WriteLine($"Name: {nameEl.GetString()}");
|
if (result.TryGetValue("name", out var name) && name is JsonElement nameEl) Console.WriteLine($"Name: {nameEl.GetString()}");
|
||||||
|
|
@ -369,7 +455,7 @@ async Task CmdServiceAsync(Args args)
|
||||||
var envContent = BuildEnvContent(args.Env, args.EnvFile);
|
var envContent = BuildEnvContent(args.Env, args.EnvFile);
|
||||||
if (!string.IsNullOrEmpty(envContent))
|
if (!string.IsNullOrEmpty(envContent))
|
||||||
{
|
{
|
||||||
if (await ServiceEnvSetAsync(serviceId, envContent, publicKey, secretKey))
|
if (ServiceEnvSet(serviceId, envContent, publicKey, secretKey))
|
||||||
Console.WriteLine($"{GREEN}Vault configured with environment variables{RESET}");
|
Console.WriteLine($"{GREEN}Vault configured with environment variables{RESET}");
|
||||||
else
|
else
|
||||||
Console.Error.WriteLine($"{YELLOW}Warning: Failed to set vault{RESET}");
|
Console.Error.WriteLine($"{YELLOW}Warning: Failed to set vault{RESET}");
|
||||||
|
|
@ -382,7 +468,7 @@ async Task CmdServiceAsync(Args args)
|
||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task CmdServiceEnvAsync(Args args, string publicKey, string secretKey)
|
void CmdServiceEnv(Args args, string publicKey, string secretKey)
|
||||||
{
|
{
|
||||||
var action = args.EnvAction;
|
var action = args.EnvAction;
|
||||||
var target = args.EnvTarget;
|
var target = args.EnvTarget;
|
||||||
|
|
@ -390,7 +476,7 @@ async Task CmdServiceEnvAsync(Args args, string publicKey, string secretKey)
|
||||||
if (action == "status")
|
if (action == "status")
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env status requires service ID{RESET}"); Environment.Exit(1); }
|
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env status requires service ID{RESET}"); Environment.Exit(1); }
|
||||||
var result = await ApiRequestAsync($"/services/{target}/env", HttpMethod.Get, null, publicKey, secretKey);
|
var result = ApiRequest($"/services/{target}/env", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
if (result.TryGetValue("has_vault", out var hv) && hv is JsonElement hvEl && hvEl.GetBoolean())
|
if (result.TryGetValue("has_vault", out var hv) && hv is JsonElement hvEl && hvEl.GetBoolean())
|
||||||
{
|
{
|
||||||
Console.WriteLine($"{GREEN}Vault: configured{RESET}");
|
Console.WriteLine($"{GREEN}Vault: configured{RESET}");
|
||||||
|
|
@ -404,25 +490,239 @@ async Task CmdServiceEnvAsync(Args args, string publicKey, string secretKey)
|
||||||
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env set requires service ID{RESET}"); Environment.Exit(1); }
|
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env set requires service ID{RESET}"); Environment.Exit(1); }
|
||||||
if (args.Env.Count == 0 && string.IsNullOrEmpty(args.EnvFile)) { Console.Error.WriteLine($"{RED}Error: service env set requires -e or --env-file{RESET}"); Environment.Exit(1); }
|
if (args.Env.Count == 0 && string.IsNullOrEmpty(args.EnvFile)) { Console.Error.WriteLine($"{RED}Error: service env set requires -e or --env-file{RESET}"); Environment.Exit(1); }
|
||||||
var envContent = BuildEnvContent(args.Env, args.EnvFile);
|
var envContent = BuildEnvContent(args.Env, args.EnvFile);
|
||||||
if (await ServiceEnvSetAsync(target, envContent, publicKey, secretKey))
|
if (ServiceEnvSet(target, envContent, publicKey, secretKey))
|
||||||
Console.WriteLine($"{GREEN}Vault updated for service {target}{RESET}");
|
Console.WriteLine($"{GREEN}Vault updated for service {target}{RESET}");
|
||||||
else { Console.Error.WriteLine($"{RED}Error: Failed to update vault{RESET}"); Environment.Exit(1); }
|
else { Console.Error.WriteLine($"{RED}Error: Failed to update vault{RESET}"); Environment.Exit(1); }
|
||||||
}
|
}
|
||||||
else if (action == "export")
|
else if (action == "export")
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env export requires service ID{RESET}"); Environment.Exit(1); }
|
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env export requires service ID{RESET}"); Environment.Exit(1); }
|
||||||
var result = await ApiRequestAsync($"/services/{target}/env/export", HttpMethod.Post, null, publicKey, secretKey);
|
var result = ApiRequest($"/services/{target}/env/export", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
if (result.TryGetValue("content", out var content) && content is JsonElement contentEl) Console.Write(contentEl.GetString());
|
if (result.TryGetValue("content", out var content) && content is JsonElement contentEl) Console.Write(contentEl.GetString());
|
||||||
}
|
}
|
||||||
else if (action == "delete")
|
else if (action == "delete")
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env delete requires service ID{RESET}"); Environment.Exit(1); }
|
if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine($"{RED}Error: service env delete requires service ID{RESET}"); Environment.Exit(1); }
|
||||||
await ApiRequestAsync($"/services/{target}/env", HttpMethod.Delete, null, publicKey, secretKey);
|
ApiRequest($"/services/{target}/env", HttpMethod.Delete, null, publicKey, secretKey);
|
||||||
Console.WriteLine($"{GREEN}Vault deleted for service {target}{RESET}");
|
Console.WriteLine($"{GREEN}Vault deleted for service {target}{RESET}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<Dictionary<string, object>> ApiRequestAsync(string endpoint, HttpMethod method, Dictionary<string, object>? data, string publicKey, string secretKey)
|
void CmdSnapshot(Args args)
|
||||||
|
{
|
||||||
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
|
|
||||||
|
if (args.SnapshotList)
|
||||||
|
{
|
||||||
|
var result = ApiRequest("/snapshots", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
|
if (result.TryGetValue("snapshots", out var snapshots) && snapshots is JsonElement snapshotsEl)
|
||||||
|
{
|
||||||
|
var snapshotList = snapshotsEl.EnumerateArray().ToList();
|
||||||
|
if (snapshotList.Count == 0) { Console.WriteLine("No snapshots"); return; }
|
||||||
|
Console.WriteLine($"{"ID",-40} {"Name",-20} {"Type",-10} {"Status",-10} {"Created"}");
|
||||||
|
foreach (var s in snapshotList)
|
||||||
|
Console.WriteLine($"{GetStr(s, "id"),-40} {GetStr(s, "name"),-20} {GetStr(s, "source_type"),-10} {GetStr(s, "status"),-10} {GetStr(s, "created_at")}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SnapshotInfo != null)
|
||||||
|
{
|
||||||
|
var result = ApiRequest($"/snapshots/{args.SnapshotInfo}", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SnapshotDelete != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/snapshots/{args.SnapshotDelete}", HttpMethod.Delete, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Snapshot deleted: {args.SnapshotDelete}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SnapshotLock != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/snapshots/{args.SnapshotLock}/lock", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Snapshot locked: {args.SnapshotLock}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SnapshotUnlock != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/snapshots/{args.SnapshotUnlock}/unlock", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Snapshot unlocked: {args.SnapshotUnlock}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.SnapshotClone != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object> { ["type"] = args.SnapshotCloneType ?? "session" };
|
||||||
|
if (args.ServiceName != null) payload["name"] = args.ServiceName;
|
||||||
|
if (args.SessionShell != null) payload["shell"] = args.SessionShell;
|
||||||
|
if (args.ServicePorts != null) payload["ports"] = args.ServicePorts.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
||||||
|
if (args.Network != null) payload["network"] = args.Network;
|
||||||
|
|
||||||
|
var result = ApiRequest($"/snapshots/{args.SnapshotClone}/clone", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||||
|
Console.WriteLine($"{GREEN}Cloned to {args.SnapshotCloneType ?? "session"}: {id}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"{RED}Error: Use --list, --info, --delete, --lock, --unlock, or --clone{RESET}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CmdImage(Args args)
|
||||||
|
{
|
||||||
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
|
|
||||||
|
if (args.ImageList)
|
||||||
|
{
|
||||||
|
var result = ApiRequest("/images", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
|
if (result.TryGetValue("images", out var images) && images is JsonElement imagesEl)
|
||||||
|
{
|
||||||
|
var imageList = imagesEl.EnumerateArray().ToList();
|
||||||
|
if (imageList.Count == 0) { Console.WriteLine("No images"); return; }
|
||||||
|
Console.WriteLine($"{"ID",-40} {"Name",-20} {"Visibility",-12} {"Status",-10} {"Created"}");
|
||||||
|
foreach (var img in imageList)
|
||||||
|
Console.WriteLine($"{GetStr(img, "id"),-40} {GetStr(img, "name"),-20} {GetStr(img, "visibility"),-12} {GetStr(img, "status"),-10} {GetStr(img, "created_at")}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageInfo != null)
|
||||||
|
{
|
||||||
|
var result = ApiRequest($"/images/{args.ImageInfo}", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageDelete != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/images/{args.ImageDelete}", HttpMethod.Delete, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Image deleted: {args.ImageDelete}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageLock != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/images/{args.ImageLock}/lock", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Image locked: {args.ImageLock}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageUnlock != null)
|
||||||
|
{
|
||||||
|
ApiRequest($"/images/{args.ImageUnlock}/unlock", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Image unlocked: {args.ImageUnlock}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImagePublish != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object> { ["source_id"] = args.ImagePublish };
|
||||||
|
if (args.ImageSourceType != null) payload["source_type"] = args.ImageSourceType;
|
||||||
|
if (args.ServiceName != null) payload["name"] = args.ServiceName;
|
||||||
|
|
||||||
|
var result = ApiRequest("/images", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||||
|
Console.WriteLine($"{GREEN}Image published: {id}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageVisibility != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object> { ["visibility"] = args.ImageVisibilityMode ?? "private" };
|
||||||
|
ApiRequest($"/images/{args.ImageVisibility}", new HttpMethod("PATCH"), payload, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Image visibility set to {args.ImageVisibilityMode}: {args.ImageVisibility}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageSpawn != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.ServiceName != null) payload["name"] = args.ServiceName;
|
||||||
|
if (args.ServicePorts != null) payload["ports"] = args.ServicePorts.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
||||||
|
if (args.Network != null) payload["network"] = args.Network;
|
||||||
|
|
||||||
|
var result = ApiRequest($"/images/{args.ImageSpawn}/spawn", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||||
|
Console.WriteLine($"{GREEN}Service spawned: {id}{RESET}");
|
||||||
|
if (result.TryGetValue("url", out var url) && url is JsonElement urlEl) Console.WriteLine($"URL: {urlEl.GetString()}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ImageClone != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.ServiceName != null) payload["name"] = args.ServiceName;
|
||||||
|
|
||||||
|
var result = ApiRequest($"/images/{args.ImageClone}/clone", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
|
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||||
|
Console.WriteLine($"{GREEN}Image cloned: {id}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"{RED}Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone{RESET}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CmdLanguages(Args args)
|
||||||
|
{
|
||||||
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
var cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unsandbox");
|
||||||
|
var cachePath = Path.Combine(cacheDir, "languages.json");
|
||||||
|
var cacheMaxAge = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
List<string>? languages = null;
|
||||||
|
|
||||||
|
if (File.Exists(cachePath))
|
||||||
|
{
|
||||||
|
var cacheAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(cachePath);
|
||||||
|
if (cacheAge < cacheMaxAge)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheContent = File.ReadAllText(cachePath);
|
||||||
|
languages = JsonSerializer.Deserialize<List<string>>(cacheContent);
|
||||||
|
}
|
||||||
|
catch { /* Cache corrupted, fetch fresh */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languages == null)
|
||||||
|
{
|
||||||
|
var result = ApiRequest("/languages", HttpMethod.Get, null, publicKey, secretKey);
|
||||||
|
if (result.TryGetValue("languages", out var langsObj) && langsObj is JsonElement langsEl)
|
||||||
|
{
|
||||||
|
languages = langsEl.EnumerateArray().Select(l => l.GetString() ?? "").Where(l => !string.IsNullOrEmpty(l)).ToList();
|
||||||
|
|
||||||
|
// Save to cache
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
File.WriteAllText(cachePath, JsonSerializer.Serialize(languages));
|
||||||
|
}
|
||||||
|
catch { /* Cache write failed, continue anyway */ }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"{RED}Error: Failed to fetch languages{RESET}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.LanguagesJson)
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(languages));
|
||||||
|
else
|
||||||
|
foreach (var lang in languages)
|
||||||
|
Console.WriteLine(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, object> ApiRequest(string endpoint, HttpMethod method, Dictionary<string, object>? data, string publicKey, string secretKey)
|
||||||
{
|
{
|
||||||
var body = data != null ? JsonSerializer.Serialize(data, jsonOptions) : "";
|
var body = data != null ? JsonSerializer.Serialize(data, jsonOptions) : "";
|
||||||
|
|
||||||
|
|
@ -445,8 +745,10 @@ async Task<Dictionary<string, object>> ApiRequestAsync(string endpoint, HttpMeth
|
||||||
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await httpClient.SendAsync(request);
|
// Synchronous HTTP call
|
||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
var response = httpClient.Send(request);
|
||||||
|
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||||
|
var responseBody = reader.ReadToEnd();
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
|
@ -472,7 +774,7 @@ async Task<Dictionary<string, object>> ApiRequestAsync(string endpoint, HttpMeth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<bool> ServiceEnvSetAsync(string serviceId, string envContent, string publicKey, string secretKey)
|
bool ServiceEnvSet(string serviceId, string envContent, string publicKey, string secretKey)
|
||||||
{
|
{
|
||||||
if (envContent.Length > 65536) { Console.Error.WriteLine($"{RED}Error: Env content exceeds maximum size of 64KB{RESET}"); return false; }
|
if (envContent.Length > 65536) { Console.Error.WriteLine($"{RED}Error: Env content exceeds maximum size of 64KB{RESET}"); return false; }
|
||||||
|
|
||||||
|
|
@ -489,7 +791,7 @@ async Task<bool> ServiceEnvSetAsync(string serviceId, string envContent, string
|
||||||
request.Headers.Add("X-Timestamp", timestamp.ToString());
|
request.Headers.Add("X-Timestamp", timestamp.ToString());
|
||||||
request.Headers.Add("X-Signature", signature);
|
request.Headers.Add("X-Signature", signature);
|
||||||
|
|
||||||
var response = await httpClient.SendAsync(request);
|
var response = httpClient.Send(request);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
|
|
@ -544,6 +846,9 @@ Args ParseArgs(string[] args)
|
||||||
else if (arg == "--version") result.ShowVersion = true;
|
else if (arg == "--version") result.ShowVersion = true;
|
||||||
else if (arg == "session") result.Command = "session";
|
else if (arg == "session") result.Command = "session";
|
||||||
else if (arg == "service") result.Command = "service";
|
else if (arg == "service") result.Command = "service";
|
||||||
|
else if (arg == "snapshot") result.Command = "snapshot";
|
||||||
|
else if (arg == "image") result.Command = "image";
|
||||||
|
else if (arg == "languages") result.Command = "languages";
|
||||||
else if (arg == "key") result.Command = "key";
|
else if (arg == "key") result.Command = "key";
|
||||||
else if (arg == "env" && result.Command == "service")
|
else if (arg == "env" && result.Command == "service")
|
||||||
{
|
{
|
||||||
|
|
@ -561,19 +866,68 @@ Args ParseArgs(string[] args)
|
||||||
else if (arg == "-f" || arg == "--files") result.Files.Add(args[++i]);
|
else if (arg == "-f" || arg == "--files") result.Files.Add(args[++i]);
|
||||||
else if (arg == "-a" || arg == "--artifacts") result.Artifacts = true;
|
else if (arg == "-a" || arg == "--artifacts") result.Artifacts = true;
|
||||||
else if (arg == "-o" || arg == "--output-dir") result.OutputDir = args[++i];
|
else if (arg == "-o" || arg == "--output-dir") result.OutputDir = args[++i];
|
||||||
else if (arg == "-l" || arg == "--list") { if (result.Command == "session") result.SessionList = true; else if (result.Command == "service") result.ServiceList = true; }
|
else if (arg == "-l" || arg == "--list")
|
||||||
|
{
|
||||||
|
if (result.Command == "session") result.SessionList = true;
|
||||||
|
else if (result.Command == "service") result.ServiceList = true;
|
||||||
|
else if (result.Command == "snapshot") result.SnapshotList = true;
|
||||||
|
else if (result.Command == "image") result.ImageList = true;
|
||||||
|
}
|
||||||
else if (arg == "-s" || arg == "--shell") result.SessionShell = args[++i];
|
else if (arg == "-s" || arg == "--shell") result.SessionShell = args[++i];
|
||||||
else if (arg == "--kill") result.SessionKill = args[++i];
|
else if (arg == "--kill") result.SessionKill = args[++i];
|
||||||
else if (arg == "--name") result.ServiceName = args[++i];
|
else if (arg == "--name") result.ServiceName = args[++i];
|
||||||
|
else if (arg == "--snapshot-name") result.SnapshotName = args[++i];
|
||||||
|
else if (arg == "--hot") result.SnapshotHot = true;
|
||||||
else if (arg == "--ports") result.ServicePorts = args[++i];
|
else if (arg == "--ports") result.ServicePorts = args[++i];
|
||||||
else if (arg == "--type") result.ServiceType = args[++i];
|
else if (arg == "--type") result.ServiceType = args[++i];
|
||||||
else if (arg == "--bootstrap") result.ServiceBootstrap = args[++i];
|
else if (arg == "--bootstrap") result.ServiceBootstrap = args[++i];
|
||||||
else if (arg == "--info") result.ServiceInfo = args[++i];
|
else if (arg == "--bootstrap-file") result.ServiceBootstrapFile = args[++i];
|
||||||
|
else if (arg == "--info")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "service") result.ServiceInfo = val;
|
||||||
|
else if (result.Command == "snapshot") result.SnapshotInfo = val;
|
||||||
|
else if (result.Command == "image") result.ImageInfo = val;
|
||||||
|
}
|
||||||
else if (arg == "--logs") result.ServiceLogs = args[++i];
|
else if (arg == "--logs") result.ServiceLogs = args[++i];
|
||||||
else if (arg == "--tail") result.ServiceTail = args[++i];
|
else if (arg == "--tail") result.ServiceTail = args[++i];
|
||||||
else if (arg == "--freeze") result.ServiceSleep = args[++i];
|
else if (arg == "--freeze")
|
||||||
else if (arg == "--unfreeze") result.ServiceWake = args[++i];
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "session") result.SessionFreeze = val;
|
||||||
|
else result.ServiceSleep = val;
|
||||||
|
}
|
||||||
|
else if (arg == "--unfreeze")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "session") result.SessionUnfreeze = val;
|
||||||
|
else result.ServiceWake = val;
|
||||||
|
}
|
||||||
|
else if (arg == "--boost") result.SessionBoost = args[++i];
|
||||||
|
else if (arg == "--unboost") result.SessionUnboost = args[++i];
|
||||||
|
else if (arg == "--snapshot")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "session") result.SessionSnapshot = val;
|
||||||
|
else if (result.Command == "service") result.ServiceSnapshot = val;
|
||||||
|
}
|
||||||
else if (arg == "--destroy") result.ServiceDestroy = args[++i];
|
else if (arg == "--destroy") result.ServiceDestroy = args[++i];
|
||||||
|
else if (arg == "--lock")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "service") result.ServiceLock = val;
|
||||||
|
else if (result.Command == "snapshot") result.SnapshotLock = val;
|
||||||
|
else if (result.Command == "image") result.ImageLock = val;
|
||||||
|
}
|
||||||
|
else if (arg == "--unlock")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "service") result.ServiceUnlock = val;
|
||||||
|
else if (result.Command == "snapshot") result.SnapshotUnlock = val;
|
||||||
|
else if (result.Command == "image") result.ImageUnlock = val;
|
||||||
|
}
|
||||||
|
else if (arg == "--resize") result.ServiceResize = args[++i];
|
||||||
|
else if (arg == "--redeploy") result.ServiceRedeploy = args[++i];
|
||||||
else if (arg == "--execute") result.ServiceExecute = args[++i];
|
else if (arg == "--execute") result.ServiceExecute = args[++i];
|
||||||
else if (arg == "--command") result.ServiceCommand = args[++i];
|
else if (arg == "--command") result.ServiceCommand = args[++i];
|
||||||
else if (arg == "--dump-bootstrap") result.ServiceDumpBootstrap = args[++i];
|
else if (arg == "--dump-bootstrap") result.ServiceDumpBootstrap = args[++i];
|
||||||
|
|
@ -582,6 +936,28 @@ Args ParseArgs(string[] args)
|
||||||
else if (arg == "--unfreeze-on-demand-enabled") result.ServiceUnfreezeOnDemandEnabled = args[++i].ToLower() == "true";
|
else if (arg == "--unfreeze-on-demand-enabled") result.ServiceUnfreezeOnDemandEnabled = args[++i].ToLower() == "true";
|
||||||
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
|
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
|
||||||
else if (arg == "--extend") result.KeyExtend = true;
|
else if (arg == "--extend") result.KeyExtend = true;
|
||||||
|
else if (arg == "--delete")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "snapshot") result.SnapshotDelete = val;
|
||||||
|
else if (result.Command == "image") result.ImageDelete = val;
|
||||||
|
}
|
||||||
|
else if (arg == "--clone")
|
||||||
|
{
|
||||||
|
var val = args[++i];
|
||||||
|
if (result.Command == "snapshot") result.SnapshotClone = val;
|
||||||
|
else if (result.Command == "image") result.ImageClone = val;
|
||||||
|
}
|
||||||
|
else if (arg == "--clone-type") result.SnapshotCloneType = args[++i];
|
||||||
|
else if (arg == "--publish") result.ImagePublish = args[++i];
|
||||||
|
else if (arg == "--source-type") result.ImageSourceType = args[++i];
|
||||||
|
else if (arg == "--visibility")
|
||||||
|
{
|
||||||
|
result.ImageVisibility = args[++i];
|
||||||
|
if (i + 1 < args.Length && !args[i + 1].StartsWith("-")) result.ImageVisibilityMode = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg == "--spawn") result.ImageSpawn = args[++i];
|
||||||
|
else if (arg == "--json") result.LanguagesJson = true;
|
||||||
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -589,12 +965,15 @@ Args ParseArgs(string[] args)
|
||||||
|
|
||||||
void PrintHelp()
|
void PrintHelp()
|
||||||
{
|
{
|
||||||
Console.WriteLine($@"un {VERSION} (.NET 10) - Unsandbox CLI
|
Console.WriteLine($@"un {VERSION} (.NET 10 sync) - Unsandbox CLI
|
||||||
|
|
||||||
Usage: dotnet run -- [options] <source_file>
|
Usage: dotnet run -- [options] <source_file>
|
||||||
dotnet run -- session [options]
|
dotnet run -- session [options]
|
||||||
dotnet run -- service [options]
|
dotnet run -- service [options]
|
||||||
dotnet run -- service env <action> <service_id> [options]
|
dotnet run -- service env <action> <service_id> [options]
|
||||||
|
dotnet run -- snapshot [options]
|
||||||
|
dotnet run -- image [options]
|
||||||
|
dotnet run -- languages [options]
|
||||||
dotnet run -- key [options]
|
dotnet run -- key [options]
|
||||||
|
|
||||||
Execute options:
|
Execute options:
|
||||||
|
|
@ -610,6 +989,13 @@ Session options:
|
||||||
--list List active sessions
|
--list List active sessions
|
||||||
--shell NAME Shell/REPL to use
|
--shell NAME Shell/REPL to use
|
||||||
--kill ID Terminate session
|
--kill ID Terminate session
|
||||||
|
--freeze ID Freeze session
|
||||||
|
--unfreeze ID Unfreeze session
|
||||||
|
--boost ID Boost session resources
|
||||||
|
--unboost ID Remove session boost
|
||||||
|
--snapshot ID Create snapshot from session
|
||||||
|
--snapshot-name Name for snapshot
|
||||||
|
--hot Live snapshot (no freeze)
|
||||||
|
|
||||||
Service options:
|
Service options:
|
||||||
--list List services
|
--list List services
|
||||||
|
|
@ -617,11 +1003,17 @@ Service options:
|
||||||
--ports PORTS Comma-separated ports
|
--ports PORTS Comma-separated ports
|
||||||
--type TYPE Service type (minecraft/mumble/teamspeak/source/tcp/udp)
|
--type TYPE Service type (minecraft/mumble/teamspeak/source/tcp/udp)
|
||||||
--bootstrap CMD Bootstrap command
|
--bootstrap CMD Bootstrap command
|
||||||
|
--bootstrap-file FILE Bootstrap from file
|
||||||
--info ID Get service details
|
--info ID Get service details
|
||||||
--logs ID Get all logs
|
--logs ID Get all logs
|
||||||
--tail ID Get last 9000 lines
|
--tail ID Get last 9000 lines
|
||||||
--freeze ID Freeze service
|
--freeze ID Freeze service
|
||||||
--unfreeze ID Unfreeze service
|
--unfreeze ID Unfreeze service
|
||||||
|
--lock ID Prevent deletion
|
||||||
|
--unlock ID Allow deletion
|
||||||
|
--resize ID Resize (use with -v)
|
||||||
|
--redeploy ID Re-run bootstrap
|
||||||
|
--snapshot ID Create snapshot from service
|
||||||
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
||||||
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
||||||
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
||||||
|
|
@ -639,6 +1031,32 @@ Service env commands:
|
||||||
env export ID Export vault contents
|
env export ID Export vault contents
|
||||||
env delete ID Delete vault
|
env delete ID Delete vault
|
||||||
|
|
||||||
|
Snapshot options:
|
||||||
|
--list List all snapshots
|
||||||
|
--info ID Get snapshot details
|
||||||
|
--delete ID Delete snapshot
|
||||||
|
--lock ID Prevent deletion
|
||||||
|
--unlock ID Allow deletion
|
||||||
|
--clone ID Clone snapshot to session/service
|
||||||
|
--clone-type TYPE Clone type: session or service
|
||||||
|
--name NAME Name for cloned resource
|
||||||
|
--ports PORTS Ports for cloned service
|
||||||
|
|
||||||
|
Image options:
|
||||||
|
--list List all images
|
||||||
|
--info ID Get image details
|
||||||
|
--delete ID Delete image
|
||||||
|
--lock ID Prevent deletion
|
||||||
|
--unlock ID Allow deletion
|
||||||
|
--publish ID Publish from service/snapshot
|
||||||
|
--source-type TYPE Source type: service or snapshot
|
||||||
|
--visibility ID MODE Set visibility (private/unlisted/public)
|
||||||
|
--spawn ID Spawn new service from image
|
||||||
|
--clone ID Clone image
|
||||||
|
|
||||||
|
Languages options:
|
||||||
|
--json Output as JSON array
|
||||||
|
|
||||||
Key options:
|
Key options:
|
||||||
--extend Open browser to extend expired key
|
--extend Open browser to extend expired key
|
||||||
|
|
||||||
|
|
@ -655,8 +1073,10 @@ class Args
|
||||||
public List<string> Env = new(), Files = new();
|
public List<string> Env = new(), Files = new();
|
||||||
public bool Artifacts, SessionList, ServiceList;
|
public bool Artifacts, SessionList, ServiceList;
|
||||||
public string? SessionShell, SessionKill;
|
public string? SessionShell, SessionKill;
|
||||||
public string? ServiceName, ServicePorts, ServiceBootstrap, ServiceType;
|
public string? SessionFreeze, SessionUnfreeze, SessionBoost, SessionUnboost, SessionSnapshot;
|
||||||
|
public string? ServiceName, ServicePorts, ServiceBootstrap, ServiceBootstrapFile, ServiceType;
|
||||||
public string? ServiceInfo, ServiceLogs, ServiceTail, ServiceSleep, ServiceWake, ServiceDestroy;
|
public string? ServiceInfo, ServiceLogs, ServiceTail, ServiceSleep, ServiceWake, ServiceDestroy;
|
||||||
|
public string? ServiceLock, ServiceUnlock, ServiceResize, ServiceRedeploy, ServiceSnapshot;
|
||||||
public string? ServiceExecute, ServiceCommand;
|
public string? ServiceExecute, ServiceCommand;
|
||||||
public string? ServiceDumpBootstrap, ServiceDumpFile;
|
public string? ServiceDumpBootstrap, ServiceDumpFile;
|
||||||
public string? ServiceUnfreezeOnDemand;
|
public string? ServiceUnfreezeOnDemand;
|
||||||
|
|
@ -664,4 +1084,13 @@ class Args
|
||||||
public bool ServiceCreateUnfreezeOnDemand;
|
public bool ServiceCreateUnfreezeOnDemand;
|
||||||
public string? EnvFile, EnvAction, EnvTarget;
|
public string? EnvFile, EnvAction, EnvTarget;
|
||||||
public bool KeyExtend;
|
public bool KeyExtend;
|
||||||
|
public bool SnapshotList;
|
||||||
|
public string? SnapshotInfo, SnapshotDelete, SnapshotLock, SnapshotUnlock, SnapshotClone;
|
||||||
|
public string? SnapshotCloneType, SnapshotName;
|
||||||
|
public bool SnapshotHot;
|
||||||
|
public bool ImageList;
|
||||||
|
public string? ImageInfo, ImageDelete, ImageLock, ImageUnlock;
|
||||||
|
public string? ImagePublish, ImageSourceType, ImageVisibility, ImageVisibilityMode;
|
||||||
|
public string? ImageSpawn, ImageClone;
|
||||||
|
public bool LanguagesJson;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue