using System.Reflection; using LibGit2Sharp; using CommandLine; using Spectre.Console; using DotNet = Microsoft.DotNet.Cli.Utils; namespace RobloxCS.CLI { public static class Program { private const string _exampleProjectRepo = "https://github.com/roblox-csharp/example-project.git"; private static readonly Assembly _compilerAssembly = Assembly.Load("RobloxCS"); private static readonly System.Version _version = _compilerAssembly.GetName().Version!; public class Options { [Option('v', "version", Required = false, HelpText = "Return the compiler version.")] public bool Version { get; set; } [Option('w', "watch", Required = false, HelpText = "Watches the directory and re-compiles when changes are detected.")] public bool WatchMode { get; set; } [Option("init", Required = false, HelpText = "Initialize a new roblox-cs project.")] public bool Init { get; set; } [Value(0, Required = false, HelpText = "Project directory path", MetaName = "Path")] public string? Path { get; set; } } public static void Main(string[] args) { Parser.Default.ParseArguments(args) .WithParsed(options => { var path = options.Path ?? "."; if (options.Version) { Console.WriteLine($"roblox-cs {_version.Major}.{_version.Minor}.{_version.Build}"); } else if (options.Init) { InitializeProject(path); } else if (options.WatchMode) { StartWatchMode(path); } else { new Transpiler(path).Transpile(); } }); } private static void StartWatchMode(string path) { var transpiler = new Transpiler(path); var watcher = new FileSystemWatcher { Path = path, NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size, Filter = "*.*", IncludeSubdirectories = true }; watcher.Changed += (sender, e) => OnFileChanged(path, e); watcher.Created += (sender, e) => OnFileChanged(path, e); watcher.Renamed += (sender, e) => OnFileChanged(path, e); watcher.Deleted += (sender, e) => OnFileChanged(path, e); watcher.EnableRaisingEvents = true; AnsiConsole.MarkupLine("[yellow]Watching for file changes. Press \"enter\" to exit.[/]"); Console.WriteLine(); Console.ReadLine(); } private static void OnFileChanged(string path, FileSystemEventArgs e) { HashSet validChangeTypes = [WatcherChangeTypes.Changed, WatcherChangeTypes.Created, WatcherChangeTypes.Renamed]; if (!validChangeTypes.Contains(e.ChangeType)) return; var filePath = Utility.FixPathSep(e.FullPath); var extension = Path.GetExtension(filePath); var transpiler = new Transpiler(path); var isValidLuaFile = extension == ".lua" && !filePath.EndsWith("include/RuntimeLib.lua") && !filePath.Contains($"/{transpiler.Config.OutputFolder}/"); if (extension == ".cs" || isValidLuaFile || extension == ".yml") { AnsiConsole.MarkupLine($"[yellow]{(e.ChangeType == WatcherChangeTypes.Renamed ? "Changed" : e.ChangeType)}: {filePath}[/]"); transpiler.Transpile(); } } private static void InitializeProject(string path) { var projectName = Path.GetDirectoryName(path) == "" ? Path.GetFileName(path) : Path.GetDirectoryName(path) ?? "Example"; try { Repository.Clone(_exampleProjectRepo, path); AnsiConsole.MarkupLine($"[cyan]Repository cloned to {Path.GetFullPath(path)}[/]"); } catch (Exception ex) { AnsiConsole.MarkupLine($"[red]Failed to clone example project repository: {ex.Message}[/]"); Environment.Exit(1); } DeleteDirectoryManual(Path.Combine(path, ".git")); DotNet.Command.Create("dotnet", ["restore", path]).Execute(); AnsiConsole.MarkupLine("[cyan]Successfully restored .NET project.[/]"); if (projectName != "Example") { var projectFolder = path; var projectSourceFolder = Path.Combine(projectFolder, projectName, "src"); var projectFile = Path.Combine(projectSourceFolder, $"{projectName}.csproj"); var rojoManifestFile = Path.Combine(projectFolder, projectName, "default.project.json"); AnsiConsole.MarkupLine("[yellow]Renaming project...[/]"); File.Delete(Path.Combine(path, "Example.sln")); DotNet.Command.Create("dotnet", ["new", "sln", "-n", projectName, "-o", path]).Execute(); Directory.Move(Path.Combine(path, "Example"), Path.Combine(projectFolder, projectName)); File.Move(Path.Combine(projectSourceFolder, "Example.csproj"), projectFile); // Dynamicallty replace ... inside of the csproj file. This sucks, and we must edit the solution to not lose the reference to it. var lines = File.ReadAllText(projectFile); var start = lines.IndexOf("", StringComparison.InvariantCulture) + "<Title>".Length; var end = lines.IndexOf("", StringComparison.InvariantCulture); lines = lines.Remove(start, end - start); lines = lines.Insert(start, projectName); File.WriteAllText(projectFile, lines); DotNet.Command.Create("dotnet", ["sln", Path.Combine(path, $"{projectName}.sln"), "add", projectFile]).Execute(); AnsiConsole.MarkupLine("[cyan]Successfully renamed project.[/]"); AnsiConsole.MarkupLine("[yellow]Modifying rojo manifest...[/]"); var rojoManifest = File.ReadAllText(rojoManifestFile); // Match for the name JSON key and its end and replace the name of it, horrible, but works. var idxOfStart = rojoManifest.IndexOf("\"name\": \"", StringComparison.InvariantCulture) + "\"name\": \"".Length; var idxOfEnd = rojoManifest.IndexOf("\",", idxOfStart, StringComparison.InvariantCulture); rojoManifest = rojoManifest.Remove(idxOfStart, idxOfEnd - idxOfStart); rojoManifest = rojoManifest.Insert(idxOfStart, projectName); File.WriteAllText(rojoManifestFile, rojoManifest); } Console.WriteLine($"Configuration:"); var initRepo = AnsiConsole.Confirm("\t[yellow]Do you want to initialize a Git repository?[/]", true); if (initRepo) { Repository.Init(path); AnsiConsole.MarkupLine("[cyan]Successfully initialized Git repository.[/]"); } } private static void DeleteDirectoryManual(string path) { if (Directory.Exists(path)) { foreach (string file in Directory.GetFiles(path)) { File.SetAttributes(file, FileAttributes.Normal); // Ensure file is not read-only File.Delete(file); } foreach (string subdir in Directory.GetDirectories(path)) { DeleteDirectoryManual(subdir); } Directory.Delete(path, recursive: true); } } } }