Utilities
The MG-CLI/Utils/ folder holds the shared, mostly-static helper layer that every command builds on. These types have no command-line surface of their own; they exist to keep the commands small, consistent, and cross-platform. This page explains what each one does and why it exists.
Log — colored logging with optional file capture
Log is the single output channel for the whole tool. It wraps Spectre.Console to write severity-colored text and, optionally, tee that same output to a log file.
Severity API (each writes a color-coded line and forwards to the file sink at a mapped level):
| Method | Color | File log level |
|---|---|---|
Log.Print(msg) |
White | Debug |
Log.Success(msg) |
Green | Info |
Log.PrintWarning(msg) |
Yellow | Warning |
Log.PrintError(msg) |
Red | Error |
Log.Logo() |
Cyan Figlet banner | — |
File sink. A command can begin capturing output to a file:
Log.CreateLogFile("builds/Logs/Windows.log", LogLevel.Debug); // minimum level to record
// ... work; all Log.* calls and piped subprocess output are teed to the file ...
Log.StopLoggingToFile();
Key behaviors:
- The
LogLevelenum (Debug < Info < Warning < Error) sets the minimum level written to the file; console output is unaffected. - The stream is opened with
AutoFlushso a crash still leaves a usable log. - ANSI escape codes are stripped before writing to disk (via a compiled
GeneratedRegex), so log files are clean plain text even though the console is colored.
godot build uses per-preset log files (builds/Logs/<preset>.log); godot import captures to godot-import.log at Warning level.
CliWrapExtensions — consistent process I/O
CliWrapExtensions makes every subprocess log the same way. It defines two shared CliWrap PipeTargets and one extension method:
command
.WithStandardOutputPipe(StdOutPipe) // → Log.Print
.WithStandardErrorPipe(StdErrorPipe); // → Log.PrintWarning if line starts with "warning", else Log.PrintError
WithCustomPipes() bundles both, and is called on nearly every Cli.Wrap(...) in the codebase:
await Cli.Wrap("git").WithArguments("add .").WithCustomPipes().ExecuteAsync(token);
The payoff: subprocess output is colored and, when a file sink is active, captured — without each command wiring up piping itself.
Web — downloads with a progress bar
Web.DownloadFileWithProgressAsync(url, destinationPath) streams an HTTP download to disk while rendering a Spectre progress bar.
- Uses
HttpCompletionOption.ResponseHeadersReadand an 8 KB buffer to stream large files without buffering them entirely in memory. - Drives the progress bar from the
Content-Lengthheader; if the server does not report a length, it retries the download. - Used by every "setup/install" command:
godot install,steam setup,itchio setup.
Zip — unzip with a progress bar
Zip.UnzipFileAsync(zipPath, extractPath) extracts a .zip archive with a progress bar driven by cumulative entry size.
- Recreates the directory structure, skips directory entries, and copies each file with a configurable buffer (default ~80 KB).
- Used to unpack the Godot engine and export templates, the Steamworks SDK, Butler, and macOS
.appbundles produced bygodot build.
FileEx — safe writes and chmod
FileEx centralizes two file concerns:
WriteAllLinesAsync(path, lines)— writes UTF-8 without a BOM. This matters for the config files MG-CLI edits (project.godot,.vdf): a stray BOM can break tools that parse them. Every command that rewrites a settings file goes through this.Chmod(filePath)— achmod +xhelper that is a no-op on Windows and shells out on Linux/macOS. Used after downloading engine/tool binaries so they are executable.
DirectoryUtil — delete/recreate helpers
DirectoryUtil provides two small guards used throughout the install and build flows:
DeleteDirectoryExists(path, recreate)— delete a directory if present, optionally recreating it empty (used to clean output/temp dirs before writing to them).CreateDirectoryIfNotExists(path)— idempotent create.
GodotUtils — locate the settings file
GodotUtils.GetProjectSettingsFile(fullPath) finds the first *.godot settings file at or below a given path (recursive search, first match). This is why the project-path arguments are forgiving: you can point at the project root and the version/settings commands still find project.godot.
StringExtensions — ToSnakeCase()
StringExtensions.ToSnakeCase() converts a string to snake_case (splitting camelCase boundaries, normalizing separators, collapsing repeats), falling back to "anim" for empty input. A small utility kept in the shared layer.
Together these utilities encode the tool's cross-cutting conventions — consistent logging, progress feedback, BOM-free writes, and platform-safe permissions — so the command classes can stay focused on orchestration. See Architecture for how they tie into the command pattern.