Add Docker image, Compose, CI/CD workflows and docs

- Multi-arch (amd64/arm64) panel image: static musl binary on scratch,
  cross-compiled with cargo-zigbuild (no QEMU), non-root, healthcheck
- docker-compose.yml + .env.example for one-command deployment
- CI: fmt, clippy, tests, web type-checks, Windows launcher build,
  real-network installs of vanilla/Fabric/Quilt/Forge/NeoForge,
  Docker build
- Release: Windows NSIS installer with baked-in panel URL, GHCR image
  push and GitHub release (tag push or manual dispatch)
- README, admin guide, Microsoft auth setup, architecture, development
- rustfmt config and formatting pass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
Claude committed 2026-09-28 05:19:36 +00:00
1 parent 50eb1a0cac
commit 5dc8a4f2f1
54 files changed
+1019 -219

No files matched your search

-1
View File
@@ -3,7 +3,6 @@ name = "scopenet-launcher"
description = "ScopeNet Minecraft launcher"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "scopenet_launcher_lib"
+2 -4
View File
@@ -45,10 +45,8 @@ impl AccountsFile {
/// Add or replace (same kind + uuid + panel) and make it active.
pub fn upsert(&mut self, account: Account) -> Account {
if let Some(existing) = self
.accounts
.iter_mut()
.find(|a| a.kind == account.kind && a.uuid == account.uuid && a.panel_url == account.panel_url)
if let Some(existing) =
self.accounts.iter_mut().find(|a| a.kind == account.kind && a.uuid == account.uuid && a.panel_url == account.panel_url)
{
let id = existing.id.clone();
*existing = Account { id: id.clone(), ..account };
+18 -3
View File
@@ -134,7 +134,15 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
let auth = auth.unwrap();
report(Event::Stage { stage: Stage::Launching, label: "Starting Minecraft".into() });
let pick = |a: u32, b: u32, c: u32| if a > 0 { a } else if b > 0 { b } else { c };
let pick = |a: u32, b: u32, c: u32| {
if a > 0 {
a
} else if b > 0 {
b
} else {
c
}
};
let max = pick(ov.memory_max_mb, settings.memory_max_mb, inst.memory.max_mb).max(512);
let min = pick(ov.memory_min_mb, settings.memory_min_mb, inst.memory.min_mb).min(max);
let mut jvm = launch::split_args(&inst.jvm_args);
@@ -200,7 +208,13 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
// Stream logs to the UI in small batches (cheap IPC), keep a tail for crash reports.
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
for stream in [child.stdout.take().map(|s| Box::new(s) as Box<dyn tokio::io::AsyncRead + Unpin + Send>), child.stderr.take().map(|s| Box::new(s) as Box<dyn tokio::io::AsyncRead + Unpin + Send>)].into_iter().flatten() {
for stream in [
child.stdout.take().map(|s| Box::new(s) as Box<dyn tokio::io::AsyncRead + Unpin + Send>),
child.stderr.take().map(|s| Box::new(s) as Box<dyn tokio::io::AsyncRead + Unpin + Send>),
]
.into_iter()
.flatten()
{
let tx = tx.clone();
tokio::spawn(async move {
let mut lines = BufReader::new(stream).lines();
@@ -268,7 +282,8 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
w.set_focus().ok();
}
}
app2.emit("game://exit", ExitPayload { instance_id: id2.clone(), code, crashed, tail: tail.into_iter().collect(), crash_report }).ok();
app2.emit("game://exit", ExitPayload { instance_id: id2.clone(), code, crashed, tail: tail.into_iter().collect(), crash_report })
.ok();
emit_state(&app2, &id2, "idle", None);
});
Ok(())