Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 49 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,88 @@
![catalyst](/.github/assets/banner.png)
![Catalyst banner](.github/assets/banner.png)

# Catalyst

[![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/Platform-Windows-lightgrey.svg)](#installation)
[![Electron](https://img.shields.io/badge/Electron-33-47848F.svg?logo=electron&logoColor=white)](https://www.electronjs.org/)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](LICENSE)
[![Platform: Windows](https://img.shields.io/badge/platform-Windows-111827.svg)](#installation)
[![Electron 33](https://img.shields.io/badge/Electron-33-47848F.svg?logo=electron&logoColor=white)](https://www.electronjs.org/)

**A modern, feature-rich Minecraft server launcher and manager.**
Catalyst is a desktop manager for Minecraft servers. It helps you create, run, configure, back up, and monitor multiple servers from one Windows app.

Catalyst makes it easy to create, configure, and manage multiple Minecraft servers from a single desktop application. With automatic Java management, plugin support via Modrinth, built-in analytics, and a sleek dark UI — running a Minecraft server has never been simpler.
## Highlights

---
- Manage multiple Vanilla, Paper, Fabric, Forge, and Purpur servers
- Download and select the right Java runtime for each Minecraft version
- Use a built-in live console for server output and commands
- Search and install plugins from Modrinth
- Track TPS, memory, player activity, and events with CatalystAnalytics
- Schedule backups for worlds and server files
- Edit server settings, whitelist, and ban list from the UI
- Share a server through the built-in ngrok integration

## ✨ Features
## Installation

- 🖥️ **Multi-Server Management** — Create and run multiple Minecraft servers (Vanilla, Paper, Fabric, Forge, and more)
- ☕ **Automatic Java Runtime** — Downloads and manages the correct Java version (8, 11, 17, 21) based on your Minecraft version
- 📟 **Real-Time Console** — Integrated server console with live output
- 🔌 **Plugin Management** — Search, install, and update plugins directly from Modrinth
- 📊 **Server Analytics** — Bundled CatalystAnalytics plugin for TPS, memory usage, player stats, and geo-location tracking
- 💾 **Automated Backups** — Configurable backup schedules to keep your worlds safe
- 🌐 **Ngrok Integration** — Share your server instantly with built-in tunnel support
- ⚙️ **Server Properties Editor** — Edit server.properties, whitelist, and banlist from the UI
- 🔄 **Auto-Updates** — Built-in update checker to stay on the latest version
- 💻 **Cross-Platform** — Supports Windows (Linux coming soon)
Download the latest Windows installer from [GitHub Releases](../../releases).

---
| Platform | Status | Format |
| --- | --- | --- |
| Windows | Supported | `.exe` installer |
| Linux | Planned | Not available yet |

## 📥 Installation
## Development

Download the latest release from [**GitHub Releases**](../../releases).
### Requirements

| Platform | Format |
|----------|--------|
| Windows | `.exe` installer |
| Linux | 🚧 Coming Soon |

---

## 🛠️ Development

### Prerequisites

- [Node.js](https://nodejs.org/) 20+
- [npm](https://www.npmjs.com/) (included with Node.js)
- [Node.js](https://nodejs.org/) 20 or newer
- npm, included with Node.js
- Windows for packaged desktop builds

### Setup

```bash
# Install dependencies
npm install

# Start in development mode
npm run dev
```

The dev command starts the Electron app and the renderer dev server.

If `npm run dev` fails with `Error: spawn EPERM`, your terminal, antivirus, or sandbox may be blocking Node from starting the esbuild/Electron child process. Run the same command from a normal trusted terminal and allow Node/Electron when Windows or your antivirus asks.

### Build

```bash
# Windows
npm run build:win
```

# Linux
# 🚧 Coming Soon
For an unpacked local build:

```bash
npm run build:unpack
```

### Testing
### Checks

```bash
# Run unit tests
npm run typecheck
npm run test:unit

# Run tests with coverage
npm run coverage
```

---
## CatalystAnalytics

The desktop app ships with `resources/plugins/CatalystAnalytics.jar`. When analytics are enabled for a Paper or Purpur server, Catalyst copies the plugin into the server's `plugins/` directory and keeps it installed on future starts.

## 🧰 Tech Stack
The plugin exposes a local REST API for dashboard data such as TPS, memory usage, joins, leaves, play time, chat counts, commands, kills, deaths, and approximate player location. Plugin details live in [catalyst-plugin/README.md](catalyst-plugin/README.md).

| Category | Technology |
|----------|------------|
| **Framework** | [Electron 33](https://www.electronjs.org/) with [electron-vite](https://electron-vite.org/) |
| **Language** | [TypeScript](https://www.typescriptlang.org/) |
| **Frontend** | [React 19](https://react.dev/) |
| **Styling** | [Tailwind CSS 3](https://tailwindcss.com/) + [shadcn/ui](https://ui.shadcn.com/) |
| **3D Graphics** | [Three.js](https://threejs.org/) |
| **Testing** | [Vitest](https://vitest.dev/) + [Testing Library](https://testing-library.com/) |
## Tech Stack

---
| Area | Tools |
| --- | --- |
| Desktop | Electron 33, electron-vite |
| UI | React 19, Tailwind CSS, shadcn/ui |
| Language | TypeScript |
| Charts and visuals | Recharts, Three.js |
| Testing | Vitest, Testing Library |

## 📄 License
## License

This project is licensed under the [GNU General Public License v3.0](LICENSE).
Catalyst is licensed under the [GNU General Public License v3.0](LICENSE).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "catalyst",
"version": "1.2.0",
"description": "Catalyst A modern Minecraft server manager",
"description": "Catalyst - A modern Minecraft server manager",
"main": "./out/main/index.js",
"author": "",
"homepage": "",
Expand Down
24 changes: 19 additions & 5 deletions src/main/lib/server-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,24 +592,38 @@ export async function stopServer(
}

return new Promise((resolve) => {
const onClose = () => {
let settled = false;
let timeout: ReturnType<typeof setTimeout>;

const finishStopped = async () => {
if (settled) return;
settled = true;
clearTimeout(timeout);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: timeout is read before TypeScript can prove it is assigned.

finishStopped reads timeout via clearTimeout(timeout), but the variable is declared without an initializer and assigned later in the same function. With strict definite assignment enabled, this can fail typechecking; defensively it should be nullable and cleared only when set.

runningServers.delete(serverId);
stopStatsPolling(serverId);
// Clear the ngrok URL from server record when server stops
updateServerSettings(serverId, { ngrokUrl: undefined }).catch(() => {});
await updateServerSettings(serverId, { ngrokUrl: undefined }).catch(() => {});
await updateServerStatus(serverId, "Offline", "0/20");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Missing error handling can leave stopServer unresolved.

finishStopped is async and is called without await. If updateServerStatus rejects, the async function rejects before reaching resolve, so the stopServer Promise can remain pending forever. Make persistence updates non-throwing before resolving, for example by catching them or using Promise.allSettled before calling resolve.

if (mainWindow && !mainWindow.isDestroyed()) {
sendStatusUpdate(mainWindow, { serverId, status: "Offline", players: "0/20" });
}
resolve({ success: true });
};

const onClose = () => {
finishStopped();
};

// Send graceful "stop" command to MC server
if (child.stdin && !child.stdin.destroyed) {
child.stdin.write("stop\n");
}

const timeout = setTimeout(() => {
child.removeListener("close", onClose);
timeout = setTimeout(() => {
child.kill("SIGKILL");
// Clear the ngrok URL from server record when server stops
updateServerSettings(serverId, { ngrokUrl: undefined }).catch(() => {});
resolve({ success: true });
setTimeout(finishStopped, 1500);
}, 15000);

child.once("close", onClose);
Expand Down
23 changes: 20 additions & 3 deletions src/renderer/src/components/AnimatedOutlet.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { useOutlet } from "react-router-dom"
import { useLocation, useOutlet } from "react-router-dom"
import { AnimatePresence, motion } from "motion/react"

export function AnimatedOutlet() {
const outlet = useOutlet()
const location = useLocation()

return (
<div className="flex min-h-full flex-col">
{outlet}
<div className="flex min-h-full flex-col overflow-hidden">
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={location.pathname}
className="flex min-h-full flex-col"
initial={{ opacity: 0, y: 18, scale: 0.985 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.992 }}
transition={{
opacity: { duration: 0.22, ease: "easeOut" },
y: { type: "spring", stiffness: 260, damping: 30, mass: 0.72 },
scale: { duration: 0.28, ease: [0.22, 1, 0.36, 1] },
}}
>
{outlet}
</motion.div>
</AnimatePresence>
</div>
)
}
7 changes: 5 additions & 2 deletions src/renderer/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function CatalystMark() {
export function AppSidebar() {
const location = useLocation()
const servers = useServerStore((state) => state.servers)
const loaded = useServerStore((state) => state.loaded)
const runningCount = servers.filter((server) => server.status === "Online").length

const isItemActive = (path: string) =>
Expand Down Expand Up @@ -74,12 +75,14 @@ export function AppSidebar() {
<span className={`status-dot ${runningCount > 0 ? "status-dot-online" : "status-dot-offline"}`} />
<div className="leading-none">
<div className="text-[12.5px] font-medium text-foreground">
{runningCount > 0
{!loaded
? "Syncing servers"
: runningCount > 0
? `${runningCount} server${runningCount === 1 ? "" : "s"} running`
: "All servers stopped"}
</div>
<div className="mt-1 text-[11px] text-muted-foreground">
{servers.length} configured
{loaded ? `${servers.length} configured` : "Loading..."}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/ui/animated-number.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface AnimatedNumberProps {
* so there are no per-frame React re-renders.
*/
export function AnimatedNumber({ value, format, className }: AnimatedNumberProps) {
const spring = useSpring(0, { stiffness: 100, damping: 22 })
const spring = useSpring(value, { stiffness: 100, damping: 22 })

useEffect(() => {
spring.set(value)
Expand Down
Loading
Loading