1 using System.ComponentModel;
2 using System.Diagnostics;
3 using System.Runtime.InteropServices;
4
5 internal static class Program
6 {
7 private const string CommandName = "sigit";
8
9 public static async Task<int> Main(string[] arguments)
10 {
11 try
12 {
13 string executablePath = ResolveExecutablePath();
14 EnsureExecutablePermissions(executablePath);
15 return await RunAsync(executablePath, arguments);
16 }
17 catch (Exception exception) when (
18 exception is FileNotFoundException or
19 PlatformNotSupportedException or
20 Win32Exception)
21 {
22 Console.Error.WriteLine($"{CommandName}: {exception.Message}");
23 return 1;
24 }
25 }
26
27 private static string ResolveExecutablePath()
28 {
29 string runtimeIdentifier = GetRuntimeIdentifier();
30 string executableName = OperatingSystem.IsWindows() ? $"{CommandName}.exe" : CommandName;
31 string executablePath = Path.GetFullPath(
32 Path.Combine(AppContext.BaseDirectory, "native", runtimeIdentifier, executableName));
33
34 if (!File.Exists(executablePath))
35 {
36 throw new FileNotFoundException(
37 $"The native {CommandName} executable for '{runtimeIdentifier}' is not bundled in this package.",
38 executablePath);
39 }
40
41 return executablePath;
42 }
43
44 private static string GetRuntimeIdentifier()
45 {
46 string operatingSystem = OperatingSystem.IsWindows()
47 ? "windows"
48 : OperatingSystem.IsMacOS()
49 ? "darwin"
50 : OperatingSystem.IsLinux()
51 ? "linux"
52 : throw new PlatformNotSupportedException(
53 $"{CommandName} does not support this operating system through the .NET tool package.");
54
55 string architecture = RuntimeInformation.ProcessArchitecture switch
56 {
57 Architecture.X64 => "x64",
58 Architecture.Arm64 => "arm64",
59 _ => throw new PlatformNotSupportedException(
60 $"{CommandName} does not support the '{RuntimeInformation.ProcessArchitecture}' architecture through the .NET tool package."),
61 };
62
63 return $"{operatingSystem}-{architecture}";
64 }
65
66 private static void EnsureExecutablePermissions(string executablePath)
67 {
68 if (OperatingSystem.IsWindows())
69 {
70 return;
71 }
72
73 UnixFileMode currentMode = File.GetUnixFileMode(executablePath);
74 UnixFileMode requiredMode = UnixFileMode.UserRead |
75 UnixFileMode.UserWrite |
76 UnixFileMode.UserExecute |
77 UnixFileMode.GroupRead |
78 UnixFileMode.GroupExecute |
79 UnixFileMode.OtherRead |
80 UnixFileMode.OtherExecute;
81
82 if ((currentMode & requiredMode) == requiredMode)
83 {
84 return;
85 }
86
87 File.SetUnixFileMode(executablePath, currentMode | requiredMode);
88 }
89
90 private static async Task<int> RunAsync(string executablePath, IReadOnlyList<string> arguments)
91 {
92 // siGit Code picks its mode from whether stdin is a TTY: a terminal gets
93 // the ratatui chat UI, a pipe gets the ACP server. Leaving all three
94 // streams unredirected makes the child inherit this process's handles,
95 // so that detection sees the real terminal (or the real pipe from an
96 // editor) rather than something this launcher introduced.
97 ProcessStartInfo startInfo = new(executablePath)
98 {
99 UseShellExecute = false,
100 WorkingDirectory = Environment.CurrentDirectory,
101 };
102
103 foreach (string argument in arguments)
104 {
105 startInfo.ArgumentList.Add(argument);
106 }
107
108 using Process process = Process.Start(startInfo)
109 ?? throw new Win32Exception($"Failed to start '{CommandName}'.");
110
111 await process.WaitForExitAsync();
112 return process.ExitCode;
113 }
114 }