Editor/SCFU.cs
using Editor;
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace Sandbox;

public static class SCFU
{
	const string BaseUrl = "https://f4pl0.com/scfu/";

	static readonly string[] BinaryFiles =
	{
		"scfu.exe",
		"scfu.dll",
		"scfu.runtimeconfig.json",
		"Microsoft.CodeAnalysis.CSharp.dll",
		"Microsoft.CodeAnalysis.dll",
	};

	static string CacheDir => Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData ), "scfu" );
	static string ExePath => Path.Combine( CacheDir, "scfu.exe" );

	[Menu( "Editor", "SCFU/Fuck", "lock" )]
	public static void Fuck() => _ = RunCommand( "fuck" );

	[Menu( "Editor", "SCFU/Unfuck", "lock_open" )]
	public static void Unfuck() => _ = RunCommand( "unfuck" );

	[Menu( "Editor", "SCFU/Configure…", "tune" )]
	public static void Configure()
	{
		var projectRoot = Project.Current?.GetRootPath();
		if ( string.IsNullOrEmpty( projectRoot ) )
		{
			EditorUtility.DisplayDialog( "SCFU", "No active project.", icon: "🙃" );
			return;
		}

		var window = new ScfuConfigWindow( projectRoot );
		window.Show();
	}

	static async Task RunCommand( string command )
	{
		try
		{
			var projectRoot = Project.Current?.GetRootPath();
			if ( string.IsNullOrEmpty( projectRoot ) )
			{
				EditorUtility.DisplayDialog( "SCFU", "No active project.", icon: "🙃" );
				return;
			}

			await Download();

			int exitCode;
			try
			{
				exitCode = await RunProcess( command, projectRoot );
			}
			finally
			{
				Cleanup();
			}

			if ( exitCode == 0 )
			{
				if (command == "fuck")
					EditorUtility.DisplayDialog( "SCFU", $"Fucking succeeded. RESTART YOUR EDITOR!", icon:"🤫" );
				else if (command == "unfuck")
					EditorUtility.DisplayDialog( "SCFU", $"Unfucking succeeded. RESTART YOUR EDITOR!", icon:"🤫" );
				else
					EditorUtility.DisplayDialog( "SCFU", $"`{command}` succeeded.", icon:"🤫" );
			}
			else
				EditorUtility.DisplayDialog( "SCFU", $"`{command}` failed (exit {exitCode}). See console.", icon: "😬" );
		}
		catch ( Exception ex )
		{
			Log.Warning( $"[SCFU] {ex}" );
			Cleanup();
			EditorUtility.DisplayDialog( "SCFU error", ex.Message, icon: "😬" );
		}
	}

	static void Cleanup()
	{
		try
		{
			if ( Directory.Exists( CacheDir ) )
				Directory.Delete( CacheDir, recursive: true );
		}
		catch ( Exception ex )
		{
			Log.Warning( $"[SCFU] Cleanup failed: {ex.Message}" );
		}
	}

	static async Task Download()
	{
		Cleanup();
		Directory.CreateDirectory( CacheDir );
		Log.Info( $"[SCFU] Downloading binary to {CacheDir}" );

		using var http = new HttpClient();
		foreach ( var file in BinaryFiles )
		{
			var url = BaseUrl + file;
			var dest = Path.Combine( CacheDir, file );

			try
			{
				using var resp = await http.GetAsync( url, HttpCompletionOption.ResponseHeadersRead );
				resp.EnsureSuccessStatusCode();
				await using var src = await resp.Content.ReadAsStreamAsync();
				await using var fs = File.Create( dest );
				await src.CopyToAsync( fs );
				Log.Info( $"[SCFU] Fetched {file}" );
			}
			catch ( Exception ex )
			{
				throw new Exception( $"Failed to download {file}: {ex.Message}", ex );
			}
		}
	}

	static async Task<int> RunProcess( string command, string projectRoot )
	{
		var startInfo = new ProcessStartInfo
		{
			FileName = ExePath,
			WorkingDirectory = CacheDir,
			UseShellExecute = false,
			CreateNoWindow = true,
			RedirectStandardOutput = true,
			RedirectStandardError = true,
		};

		startInfo.ArgumentList.Add( command );
		startInfo.ArgumentList.Add( projectRoot );

		Log.Info( $"[SCFU] Running: scfu {command} \"{projectRoot}\"" );

		using var proc = new Process { StartInfo = startInfo };
		proc.OutputDataReceived += ( _, e ) => { if ( e.Data != null ) Log.Info( $"[SCFU] {e.Data}" ); };
		proc.ErrorDataReceived += ( _, e ) => { if ( e.Data != null ) Log.Warning( $"[SCFU] {e.Data}" ); };

		proc.Start();
		proc.BeginOutputReadLine();
		proc.BeginErrorReadLine();
		await proc.WaitForExitAsync();
		return proc.ExitCode;
	}
}