Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.
- launcher/ Avalonia 12 desktop client; single-file win-x64 publish.
Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
portable install path, sidecar settings.
- server/ brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
MC subprocess, embedded Kestrel admin panel with cookie
auth + rate limiting, RCON bridge, scheduled backups,
BlueMap CLI integration with player markers + skin proxy,
friend-side whitelist request flow, world wipe with seed
selection (keep current / random / custom).
- pack/ pack.lock.json (Modrinth + manual CurseForge entries),
data-only tweak source under tweaks/, build outputs in
overrides/ (gitignored).
- scripts/ Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
plus Deploy-Brass.ps1 unified one-shot deploy with
version-bump pre-flight and daemon-state detection.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace BrassAndSigil.Server.Commands;
|
||||
|
||||
public class BaseCommandSettings : CommandSettings
|
||||
{
|
||||
[CommandOption("-c|--config <PATH>")]
|
||||
[Description("Path to server-config.json (defaults to ./server-config.json)")]
|
||||
public string ConfigPath { get; set; } = "server-config.json";
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Diagnostics;
|
||||
using BrassAndSigil.Server.Models;
|
||||
using BrassAndSigil.Server.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace BrassAndSigil.Server.Commands;
|
||||
|
||||
public sealed class CheckCommand : AsyncCommand<BaseCommandSettings>
|
||||
{
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, BaseCommandSettings settings)
|
||||
{
|
||||
var config = ServerConfig.Load(settings.ConfigPath);
|
||||
AnsiConsole.MarkupLine("[bold]Checking server install...[/]");
|
||||
var ok = true;
|
||||
|
||||
// 1. Java available?
|
||||
var javaVersion = await TryRunForOutputAsync(config.JavaPath, "-version");
|
||||
if (javaVersion is not null)
|
||||
AnsiConsole.MarkupLine($" [green]✓[/] Java reachable: {javaVersion.Split('\n')[0].Trim().EscapeMarkup()}");
|
||||
else
|
||||
{ AnsiConsole.MarkupLine($" [red]✗[/] Java not found at '{config.JavaPath}'"); ok = false; }
|
||||
|
||||
// 2. Server dir
|
||||
var serverDir = Path.GetFullPath(config.ServerDir);
|
||||
if (Directory.Exists(serverDir))
|
||||
AnsiConsole.MarkupLine($" [green]✓[/] Server dir exists: {serverDir}");
|
||||
else
|
||||
{ AnsiConsole.MarkupLine($" [yellow]?[/] Server dir missing -- run [yellow]install[/] first"); ok = false; }
|
||||
|
||||
// 3. EULA
|
||||
var eulaPath = Path.Combine(serverDir, "eula.txt");
|
||||
if (File.Exists(eulaPath) && File.ReadAllText(eulaPath).Contains("eula=true"))
|
||||
AnsiConsole.MarkupLine(" [green]✓[/] EULA accepted");
|
||||
else
|
||||
{ AnsiConsole.MarkupLine(" [yellow]?[/] EULA not accepted (re-run [yellow]install --accept-eula[/])"); ok = false; }
|
||||
|
||||
// 4. NeoForge run script
|
||||
var runScript = Path.Combine(serverDir, OperatingSystem.IsWindows() ? "run.bat" : "run.sh");
|
||||
if (File.Exists(runScript))
|
||||
AnsiConsole.MarkupLine($" [green]✓[/] Loader start script: {Path.GetFileName(runScript)}");
|
||||
else
|
||||
AnsiConsole.MarkupLine($" [yellow]?[/] No {Path.GetFileName(runScript)} -- install the NeoForge server first");
|
||||
|
||||
// 5. Manifest reachable
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
var resp = await http.GetAsync(config.ManifestUrl);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
AnsiConsole.MarkupLine($" [green]✓[/] Manifest reachable: {config.ManifestUrl}");
|
||||
else
|
||||
{ AnsiConsole.MarkupLine($" [red]✗[/] Manifest HTTP {(int)resp.StatusCode}: {config.ManifestUrl}"); ok = false; }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{ AnsiConsole.MarkupLine($" [red]✗[/] Manifest fetch error: {ex.Message.EscapeMarkup()}"); ok = false; }
|
||||
|
||||
// 6. Pack version on disk
|
||||
var packVer = Path.Combine(serverDir, "pack-version.json");
|
||||
if (File.Exists(packVer))
|
||||
AnsiConsole.MarkupLine($" [green]✓[/] Pack synced: {File.ReadAllText(packVer).Replace("\n", " ").Replace("\r", "").Trim().EscapeMarkup()}");
|
||||
else
|
||||
AnsiConsole.MarkupLine(" [yellow]?[/] Pack not synced yet (run [yellow]sync[/])");
|
||||
|
||||
AnsiConsole.MarkupLine("");
|
||||
AnsiConsole.MarkupLine(ok ? "[green]All required checks passed.[/]" : "[yellow]Some checks failed; see above.[/]");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
private static async Task<string?> TryRunForOutputAsync(string fileName, string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
});
|
||||
if (p is null) return null;
|
||||
var output = await p.StandardError.ReadToEndAsync() + await p.StandardOutput.ReadToEndAsync();
|
||||
await p.WaitForExitAsync();
|
||||
return output;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.ComponentModel;
|
||||
using BrassAndSigil.Server.Models;
|
||||
using BrassAndSigil.Server.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace BrassAndSigil.Server.Commands;
|
||||
|
||||
public sealed class InstallCommand : AsyncCommand<InstallCommand.Settings>
|
||||
{
|
||||
public sealed class Settings : BaseCommandSettings
|
||||
{
|
||||
[CommandOption("--manifest <URL>")]
|
||||
[Description("Manifest URL to bootstrap from")]
|
||||
public string? ManifestUrl { get; set; }
|
||||
|
||||
[CommandOption("--server-dir <PATH>")]
|
||||
[Description("Where to install the server (defaults to ./server)")]
|
||||
public string? ServerDir { get; set; }
|
||||
|
||||
[CommandOption("--memory <MB>")]
|
||||
[Description("RAM allocation in MB (defaults to 8192)")]
|
||||
public int? MemoryMB { get; set; }
|
||||
|
||||
[CommandOption("--accept-eula")]
|
||||
[Description("Accept the Minecraft EULA. Required for the server to actually run.")]
|
||||
public bool AcceptEula { get; set; }
|
||||
}
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
||||
{
|
||||
var config = ServerConfig.Load(settings.ConfigPath);
|
||||
if (settings.ManifestUrl != null) config.ManifestUrl = settings.ManifestUrl;
|
||||
if (settings.ServerDir != null) config.ServerDir = settings.ServerDir;
|
||||
if (settings.MemoryMB != null) config.MemoryMB = settings.MemoryMB.Value;
|
||||
if (settings.AcceptEula) config.AcceptEula = true;
|
||||
|
||||
AnsiConsole.MarkupLine("[bold yellow]Brass & Sigil Server install[/]");
|
||||
AnsiConsole.MarkupLine($" Config: {settings.ConfigPath}");
|
||||
AnsiConsole.MarkupLine($" ServerDir: {Path.GetFullPath(config.ServerDir)}");
|
||||
AnsiConsole.MarkupLine($" Manifest: {config.ManifestUrl}");
|
||||
AnsiConsole.MarkupLine($" Memory: {config.MemoryMB} MB");
|
||||
AnsiConsole.MarkupLine("");
|
||||
|
||||
if (!config.AcceptEula)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]EULA not accepted.[/] Re-run with --accept-eula to confirm you accept the");
|
||||
AnsiConsole.MarkupLine("Minecraft End User License Agreement: [blue]https://aka.ms/MinecraftEULA[/]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(config.ServerDir);
|
||||
|
||||
// Generate eula.txt
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(config.ServerDir, "eula.txt"),
|
||||
$"# Generated by brass-sigil-server install\n" +
|
||||
$"# By setting this to true you agree to the Minecraft EULA: https://aka.ms/MinecraftEULA\n" +
|
||||
$"eula=true\n");
|
||||
|
||||
// Generate a default server.properties if none exists yet
|
||||
var propsPath = Path.Combine(config.ServerDir, "server.properties");
|
||||
if (!File.Exists(propsPath))
|
||||
{
|
||||
await File.WriteAllTextAsync(propsPath, DefaultServerProperties(config));
|
||||
AnsiConsole.MarkupLine($"[grey]Wrote default server.properties[/]");
|
||||
}
|
||||
|
||||
// Generate a random RCON password if missing
|
||||
if (string.IsNullOrEmpty(config.RconPassword))
|
||||
{
|
||||
config.RconPassword = Convert.ToHexString(Guid.NewGuid().ToByteArray()).ToLowerInvariant();
|
||||
UpdateProp(propsPath, "enable-rcon", "true");
|
||||
UpdateProp(propsPath, "rcon.port", config.RconPort.ToString());
|
||||
UpdateProp(propsPath, "rcon.password", config.RconPassword);
|
||||
}
|
||||
|
||||
// Save config
|
||||
config.Save(settings.ConfigPath);
|
||||
AnsiConsole.MarkupLine($"[grey]Saved config to {settings.ConfigPath}[/]");
|
||||
|
||||
// Sync mods
|
||||
AnsiConsole.MarkupLine("");
|
||||
AnsiConsole.MarkupLine("[bold]Syncing mods from manifest...[/]");
|
||||
var sync = new ManifestSync();
|
||||
var progress = new Progress<string>(msg => AnsiConsole.MarkupLine($" [grey]{msg.EscapeMarkup()}[/]"));
|
||||
try
|
||||
{
|
||||
var result = await sync.SyncAsync(config.ManifestUrl, config.ServerDir, progress);
|
||||
AnsiConsole.MarkupLine("");
|
||||
AnsiConsole.MarkupLine($"[green]✓ Sync complete:[/] pack v{result.PackVersion}, " +
|
||||
$"{result.Downloaded} downloaded, {result.Removed} removed, " +
|
||||
$"{result.Skipped} client-only mods skipped");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗ Sync failed:[/] {ex.Message.EscapeMarkup()}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine("");
|
||||
AnsiConsole.MarkupLine("[bold green]Install complete.[/]");
|
||||
AnsiConsole.MarkupLine("Next steps:");
|
||||
AnsiConsole.MarkupLine($" 1. Download the matching NeoForge server installer for the manifest's loader version");
|
||||
AnsiConsole.MarkupLine($" (server side, into {Path.GetFullPath(config.ServerDir)}) -- the modpack manifest");
|
||||
AnsiConsole.MarkupLine($" doesn't bundle it because each loader has its own installer.");
|
||||
AnsiConsole.MarkupLine($" [blue]https://maven.neoforged.net/releases/net/neoforged/neoforge/[/]");
|
||||
AnsiConsole.MarkupLine($" 2. Run the NeoForge installer with --installServer in the server dir");
|
||||
AnsiConsole.MarkupLine($" 3. Then start: [yellow]brass-sigil-server run[/]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string DefaultServerProperties(ServerConfig config) => $@"# Brass & Sigil server defaults -- edit as needed
|
||||
motd=Brass & Sigil
|
||||
gamemode=survival
|
||||
difficulty=normal
|
||||
hardcore=false
|
||||
pvp=true
|
||||
online-mode=true
|
||||
white-list=true
|
||||
enforce-whitelist=true
|
||||
max-players=20
|
||||
view-distance=12
|
||||
simulation-distance=10
|
||||
spawn-protection=0
|
||||
enable-rcon=true
|
||||
rcon.port={config.RconPort}
|
||||
rcon.password={config.RconPassword}
|
||||
broadcast-rcon-to-ops=false
|
||||
";
|
||||
|
||||
private static void UpdateProp(string path, string key, string value)
|
||||
{
|
||||
var lines = File.ReadAllLines(path).ToList();
|
||||
var prefix = $"{key}=";
|
||||
var idx = lines.FindIndex(l => l.StartsWith(prefix));
|
||||
if (idx >= 0) lines[idx] = prefix + value;
|
||||
else lines.Add(prefix + value);
|
||||
File.WriteAllLines(path, lines);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
using BrassAndSigil.Server.Models;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace BrassAndSigil.Server.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Set or rotate the web panel admin password from the CLI.
|
||||
/// Useful when first-time-setting up before exposing the panel publicly,
|
||||
/// or rotating after a suspected leak without going through the panel UI.
|
||||
/// </summary>
|
||||
public sealed class SetPasswordCommand : Command<BaseCommandSettings>
|
||||
{
|
||||
public override int Execute(CommandContext context, BaseCommandSettings settings)
|
||||
{
|
||||
var config = ServerConfig.Load(settings.ConfigPath);
|
||||
|
||||
if (Console.IsInputRedirected)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]set-password requires an interactive terminal.[/]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine("[bold]Set admin password[/]");
|
||||
if (!string.IsNullOrEmpty(config.WebPassword))
|
||||
AnsiConsole.MarkupLine("[grey]An existing password is already set; this will overwrite it.[/]");
|
||||
|
||||
string pw1, pw2;
|
||||
while (true)
|
||||
{
|
||||
pw1 = AnsiConsole.Prompt(new TextPrompt<string>("New password (min 8 chars):").Secret());
|
||||
if (pw1.Length < 8) { AnsiConsole.MarkupLine("[red]Too short.[/]"); continue; }
|
||||
pw2 = AnsiConsole.Prompt(new TextPrompt<string>("Confirm:").Secret());
|
||||
if (pw1 != pw2) { AnsiConsole.MarkupLine("[red]Doesn't match.[/]"); continue; }
|
||||
break;
|
||||
}
|
||||
|
||||
config.WebPassword = pw1;
|
||||
config.Save(settings.ConfigPath);
|
||||
AnsiConsole.MarkupLine($"[green]✓[/] Saved to {settings.ConfigPath}.");
|
||||
AnsiConsole.MarkupLine("[grey]Restart the server for the new password to take effect.[/]");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using BrassAndSigil.Server.Models;
|
||||
using BrassAndSigil.Server.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace BrassAndSigil.Server.Commands;
|
||||
|
||||
public sealed class SyncCommand : AsyncCommand<BaseCommandSettings>
|
||||
{
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, BaseCommandSettings settings)
|
||||
{
|
||||
var config = ServerConfig.Load(settings.ConfigPath);
|
||||
AnsiConsole.MarkupLine($"[bold]Syncing[/] from [blue]{config.ManifestUrl}[/]");
|
||||
AnsiConsole.MarkupLine($"[grey]Target: {Path.GetFullPath(config.ServerDir)}[/]");
|
||||
AnsiConsole.MarkupLine("");
|
||||
|
||||
var sync = new ManifestSync();
|
||||
var progress = new Progress<string>(msg => AnsiConsole.MarkupLine($" [grey]{msg.EscapeMarkup()}[/]"));
|
||||
try
|
||||
{
|
||||
var result = await sync.SyncAsync(config.ManifestUrl, config.ServerDir, progress);
|
||||
AnsiConsole.MarkupLine("");
|
||||
AnsiConsole.MarkupLine($"[green]✓[/] pack v{result.PackVersion} | downloaded={result.Downloaded} removed={result.Removed} client-only-skipped={result.Skipped}");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗[/] {ex.Message.EscapeMarkup()}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user