Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Net.Http.Headers;
4using System.Net.Http.Json;
5using System.Text.Json;
6
7namespace CascadeIDE.Features.Forge.Infrastructure;
8
9public static class ForgeCommandExecuteClient
10{
11 private static readonly JsonSerializerOptions JsonOptions = new()
12 {
13 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
14 PropertyNameCaseInsensitive = true,
15 };
16
17 public static async Task<(bool Ok, string Message)> ExecuteAsync(
18 string baseUrl,
19 string? apiToken,
20 string slashPath,
21 string? argsTail,
22 string repo,
23 CancellationToken cancellationToken = default)
24 {
25 var normalized = baseUrl.Trim().TrimEnd('/');
26 using var http = new HttpClient { BaseAddress = new Uri(normalized), Timeout = TimeSpan.FromSeconds(30) };
27 if (!string.IsNullOrEmpty(apiToken))
28 http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
29
30 http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
31 http.DefaultRequestHeaders.Add("X-Forge-Command-Client", "cide-slash");
32
33 var payload = new
34 {
35 path = slashPath,
36 args = argsTail ?? "",
37 context = new Dictionary<string, string> { ["repo"] = repo },
38 };
39
40 using var response = await http.PostAsJsonAsync("/api/v1/commands/execute", payload, JsonOptions, cancellationToken)
41 .ConfigureAwait(false);
42 var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
43
44 if (!response.IsSuccessStatusCode)
45 return (false, FormatError(body, response.StatusCode));
46
47 try
48 {
49 using var doc = JsonDocument.Parse(body);
50 var root = doc.RootElement;
51 var kind = root.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() : null;
52 if (string.Equals(kind, "redirect", StringComparison.OrdinalIgnoreCase)
53 && root.TryGetProperty("redirectUrl", out var redirectEl))
54 {
55 var redirectUrl = redirectEl.GetString() ?? "";
56 if (redirectUrl.StartsWith('/'))
57 redirectUrl = normalized + redirectUrl;
58
59 if (!ForgeLensOpenService.TryOpenExternal(redirectUrl, out var openError))
60 return (false, openError);
61
62 return (true, $"Opened {redirectUrl}");
63 }
64
65 if (root.TryGetProperty("body", out var bodyEl))
66 return (true, FormatJsonBody(bodyEl));
67
68 if (root.TryGetProperty("error", out var errorEl))
69 return (false, errorEl.GetString() ?? "Command failed.");
70
71 return (true, body);
72 }
73 catch (JsonException)
74 {
75 return response.IsSuccessStatusCode ? (true, body) : (false, body);
76 }
77 }
78
79 private static string FormatJsonBody(JsonElement bodyEl)
80 {
81 if (bodyEl.ValueKind == JsonValueKind.String)
82 return bodyEl.GetString() ?? "";
83
84 if (bodyEl.TryGetProperty("issueUrl", out var issueUrl))
85 return issueUrl.GetString() ?? bodyEl.ToString();
86
87 if (bodyEl.TryGetProperty("mrUrl", out var mrUrl))
88 return mrUrl.GetString() ?? bodyEl.ToString();
89
90 return bodyEl.ToString();
91 }
92
93 private static string FormatError(string body, System.Net.HttpStatusCode status)
94 {
95 try
96 {
97 using var doc = JsonDocument.Parse(body);
98 if (doc.RootElement.TryGetProperty("error", out var error))
99 return error.GetString() ?? $"{(int)status}";
100 }
101 catch
102 {
103 // fall through
104 }
105
106 return string.IsNullOrWhiteSpace(body) ? $"{(int)status}" : body;
107 }
108}
109
View only · write via MCP/CIDE