diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 834855c..618917d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -157,3 +157,22 @@ jobs:
EchoHub-Client-linux-arm64.zip
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Publish Chocolatey package
+ if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ CHECKSUM=$(sha256sum EchoHub-Client-win-x64.zip | awk '{print $1}')
+
+ # Stamp version and checksum into package templates
+ sed -i "s/__VERSION__/$VERSION/g" packaging/choco/echohub.nuspec
+ sed -i "s/__VERSION__/$VERSION/g" packaging/choco/tools/chocolateyInstall.ps1
+ sed -i "s/__CHECKSUM64__/$CHECKSUM/g" packaging/choco/tools/chocolateyInstall.ps1
+
+ # Build and push the package
+ mkdir -p /tmp/choco-out
+ cd packaging/choco
+ choco pack echohub.nuspec --output-directory /tmp/choco-out
+ choco push /tmp/choco-out/echohub.*.nupkg --source https://push.chocolatey.org/ --api-key "$CHOCO_API_KEY"
+ env:
+ CHOCO_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
diff --git a/docs/articles/getting-started.md b/docs/articles/getting-started.md
index 7b7bba3..86ebd87 100644
--- a/docs/articles/getting-started.md
+++ b/docs/articles/getting-started.md
@@ -1,12 +1,33 @@
# Getting Started
-## Prerequisites
+## Install the Client
-- [.NET 10 SDK](https://dotnet.microsoft.com/download)
+### Windows (Chocolatey)
-Or grab a self-contained binary from [Releases](https://github.com/HueByte/EchoHub/releases) -- no runtime needed.
+```bash
+choco install echohub
+```
-## Docker
+### Linux / macOS
+
+```bash
+curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
+```
+
+To install a specific version or to a custom directory:
+
+```bash
+curl -sSfL .../install.sh | sh -s -- --version 0.2.8
+curl -sSfL .../install.sh | sh -s -- --install-dir /opt/echohub
+```
+
+### Manual Download
+
+Grab a self-contained binary from [Releases](https://github.com/HueByte/EchoHub/releases) -- no runtime needed.
+
+## Host a Server
+
+### Docker
The quickest way to host a server:
@@ -17,24 +38,23 @@ docker compose up -d
See the [Docker guide](docker.md) for configuration, pre-built images, and more.
-## Run the Server
+### From Source
```bash
dotnet run --project src/EchoHub.Server
```
+Requires [.NET 10 SDK](https://dotnet.microsoft.com/download).
+
On first run, the server automatically:
1. Creates `appsettings.json` from the example config
2. Generates a secure JWT secret
3. Creates the SQLite database with a `#general` channel
-## Run the Client
-
-```bash
-dotnet run --project src/EchoHub.Client
-```
+## Usage
+After installing the client, run `echohub` (or `dotnet run --project src/EchoHub.Client` from source).
Connect to a server, register an account, and start chatting.
## Connect via IRC
diff --git a/docs/changelog/v0.2.8.md b/docs/changelog/v0.2.8.md
index 0f7d6ea..d4c7a8b 100644
--- a/docs/changelog/v0.2.8.md
+++ b/docs/changelog/v0.2.8.md
@@ -21,7 +21,13 @@
- Extract `IUserService`/`UserService` — consolidate user registration, authentication, and profile management into a dedicated service, eliminating duplicated logic between `AuthController` and `ChatService`
- IRC gateway now checks ban status during authentication (previously skipped)
+## Distribution
+
+- Add Chocolatey package — `choco install echohub` for Windows users, auto-published from CI on each release
+- Add Linux/macOS install script — `curl -sSfL .../install.sh | sh` with automatic OS/arch detection
+
## CI
- Add Docker workflow — builds and pushes multi-arch (`amd64`/`arm64`) server image to GHCR on release
- Add `linux-arm64` builds to release pipeline — server and client binaries for ARM Linux (Raspberry Pi, cloud ARM instances)
+- Automate Chocolatey package publishing in release workflow (checksum calculation, pack, push)
diff --git a/packaging/choco/echohub.nuspec b/packaging/choco/echohub.nuspec
new file mode 100644
index 0000000..e908659
--- /dev/null
+++ b/packaging/choco/echohub.nuspec
@@ -0,0 +1,26 @@
+
+
+
+ echohub
+ __VERSION__
+ EchoHub
+ HueByte
+ HueByte
+ false
+ https://github.com/HueByteByte/EchoHub/blob/master/LICENSE
+ https://github.com/HueByteByte/EchoHub
+ https://github.com/HueByteByte/EchoHub
+ https://HueBytebyte.github.io/EchoHub
+ https://github.com/HueByteByte/EchoHub/issues
+ https://github.com/HueByteByte/EchoHub/tree/master/packaging/choco
+ https://raw.githubusercontent.com/HueByteByte/EchoHub/master/assets/HueByte_icon.png
+ EchoHub is a decentralized IRC-style chat application with a terminal user interface (TUI). Connect to any EchoHub server, join channels, and chat — all from your terminal. Features include SignalR real-time messaging, file sharing, custom themes, and IRC gateway compatibility.
+ Decentralized terminal chat client with IRC gateway support
+ chat irc decentralized tui terminal signalr echohub
+ https://HueBytebyte.github.io/EchoHub/changelog/v__VERSION__.html
+ Copyright (c) 2026 HueByte
+
+
+
+
+
diff --git a/packaging/choco/tools/LICENSE.txt b/packaging/choco/tools/LICENSE.txt
new file mode 100644
index 0000000..b28e337
--- /dev/null
+++ b/packaging/choco/tools/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Hue
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packaging/choco/tools/VERIFICATION.txt b/packaging/choco/tools/VERIFICATION.txt
new file mode 100644
index 0000000..62b31d3
--- /dev/null
+++ b/packaging/choco/tools/VERIFICATION.txt
@@ -0,0 +1,17 @@
+VERIFICATION
+
+To verify the package contents:
+
+1. Download the official release from:
+ https://github.com/HueByte/EchoHub/releases
+
+2. Download: EchoHub-Client-win-x64.zip
+
+3. Calculate the SHA256 checksum:
+ - PowerShell: Get-FileHash EchoHub-Client-win-x64.zip -Algorithm SHA256
+ - Linux/macOS: sha256sum EchoHub-Client-win-x64.zip
+
+4. Compare with the checksum in chocolateyInstall.ps1 (checksum64 value).
+
+LICENSE: MIT License - see LICENSE.txt in this directory or
+https://github.com/HueByte/EchoHub/blob/master/LICENSE
diff --git a/packaging/choco/tools/chocolateyInstall.ps1 b/packaging/choco/tools/chocolateyInstall.ps1
new file mode 100644
index 0000000..a483c19
--- /dev/null
+++ b/packaging/choco/tools/chocolateyInstall.ps1
@@ -0,0 +1,18 @@
+$ErrorActionPreference = 'Stop'
+
+$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
+$version = $env:chocolateyPackageVersion
+
+$packageArgs = @{
+ packageName = $env:chocolateyPackageName
+ unzipLocation = $toolsDir
+ url64bit = "https://github.com/HueByte/EchoHub/releases/download/v$version/EchoHub-Client-win-x64.zip"
+ checksum64 = '__CHECKSUM64__'
+ checksumType64 = 'sha256'
+}
+
+Install-ChocolateyZipPackage @packageArgs
+
+# Create a shim so 'echohub' is available on PATH
+$exePath = Join-Path $toolsDir 'client-win-x64' 'EchoHub.Client.exe'
+Install-BinFile -Name 'echohub' -Path $exePath
diff --git a/packaging/choco/tools/chocolateyUninstall.ps1 b/packaging/choco/tools/chocolateyUninstall.ps1
new file mode 100644
index 0000000..0e8b0d9
--- /dev/null
+++ b/packaging/choco/tools/chocolateyUninstall.ps1
@@ -0,0 +1,3 @@
+$ErrorActionPreference = 'Stop'
+
+Uninstall-BinFile -Name 'echohub'
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100644
index 0000000..576c30e
--- /dev/null
+++ b/scripts/install.sh
@@ -0,0 +1,223 @@
+#!/bin/sh
+# EchoHub Client Installer
+# Usage: curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
+#
+# Options (pass as arguments or environment variables):
+# --version X.Y.Z Install a specific version (default: latest)
+# --install-dir DIR Install to a custom directory
+# --help Show this help message
+
+set -eu
+
+REPO="HueByte/EchoHub"
+BINARY_NAME="echohub"
+INSTALL_DIR=""
+VERSION=""
+
+# ── Argument parsing ──────────────────────────────────────────────────────
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --version)
+ VERSION="$2"
+ shift 2
+ ;;
+ --install-dir)
+ INSTALL_DIR="$2"
+ shift 2
+ ;;
+ --help)
+ sed -n '2,8p' "$0" 2>/dev/null || true
+ echo ""
+ echo " curl -sSfL https://raw.githubusercontent.com/$REPO/master/scripts/install.sh | sh"
+ echo " curl ... | sh -s -- --version 0.2.8"
+ echo " curl ... | sh -s -- --install-dir /opt/echohub"
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ exit 1
+ ;;
+ esac
+done
+
+# ── Platform detection ────────────────────────────────────────────────────
+
+detect_os() {
+ case "$(uname -s)" in
+ Linux*) echo "linux" ;;
+ Darwin*) echo "osx" ;;
+ *)
+ echo "Error: Unsupported operating system: $(uname -s)" >&2
+ echo "EchoHub supports Linux and macOS. For Windows, use: choco install echohub" >&2
+ exit 1
+ ;;
+ esac
+}
+
+detect_arch() {
+ case "$(uname -m)" in
+ x86_64|amd64) echo "x64" ;;
+ aarch64|arm64) echo "arm64" ;;
+ *)
+ echo "Error: Unsupported architecture: $(uname -m)" >&2
+ echo "EchoHub supports x64 and arm64." >&2
+ exit 1
+ ;;
+ esac
+}
+
+# ── Version resolution ────────────────────────────────────────────────────
+
+resolve_version() {
+ if [ -n "$VERSION" ]; then
+ echo "$VERSION"
+ return
+ fi
+
+ # Fetch latest release tag from GitHub API
+ if command -v curl >/dev/null 2>&1; then
+ tag=$(curl -sSf "https://api.github.com/repos/$REPO/releases/latest" \
+ | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\?\([^"]*\)".*/\1/')
+ elif command -v wget >/dev/null 2>&1; then
+ tag=$(wget -qO- "https://api.github.com/repos/$REPO/releases/latest" \
+ | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\?\([^"]*\)".*/\1/')
+ else
+ echo "Error: curl or wget is required to detect the latest version." >&2
+ echo "Install curl/wget or specify a version with --version X.Y.Z" >&2
+ exit 1
+ fi
+
+ if [ -z "$tag" ]; then
+ echo "Error: Could not determine latest version from GitHub." >&2
+ exit 1
+ fi
+
+ echo "$tag"
+}
+
+# ── Install directory resolution ──────────────────────────────────────────
+
+resolve_install_dir() {
+ if [ -n "$INSTALL_DIR" ]; then
+ echo "$INSTALL_DIR"
+ return
+ fi
+
+ # Prefer /usr/local/bin if writable, otherwise ~/.local/bin
+ if [ -w "/usr/local/bin" ]; then
+ echo "/usr/local/bin"
+ else
+ local_bin="$HOME/.local/bin"
+ mkdir -p "$local_bin"
+ echo "$local_bin"
+ fi
+}
+
+# ── Download helper ───────────────────────────────────────────────────────
+
+download() {
+ url="$1"
+ output="$2"
+
+ if command -v curl >/dev/null 2>&1; then
+ curl -sSfL "$url" -o "$output"
+ elif command -v wget >/dev/null 2>&1; then
+ wget -qO "$output" "$url"
+ else
+ echo "Error: curl or wget is required." >&2
+ exit 1
+ fi
+}
+
+# ── PATH setup ────────────────────────────────────────────────────────────
+
+add_to_path() {
+ dir="$1"
+ export_line="export PATH=\"${dir}:\$PATH\" # Added by EchoHub"
+
+ added=0
+ for profile in "$HOME/.profile" "$HOME/.bashrc" "$HOME/.zshrc"; do
+ if [ -f "$profile" ]; then
+ if grep -q "$dir" "$profile" 2>/dev/null; then
+ continue # Already present
+ fi
+ printf '\n%s\n' "$export_line" >> "$profile"
+ echo " Added to PATH in $(basename "$profile")"
+ added=1
+ fi
+ done
+
+ # If no profile existed, create .profile
+ if [ "$added" -eq 0 ]; then
+ printf '\n%s\n' "$export_line" >> "$HOME/.profile"
+ echo " Added to PATH in .profile"
+ fi
+}
+
+# ── Main ──────────────────────────────────────────────────────────────────
+
+main() {
+ os=$(detect_os)
+ arch=$(detect_arch)
+ version=$(resolve_version)
+ install_dir=$(resolve_install_dir)
+
+ artifact="EchoHub-Client-${os}-${arch}.zip"
+ url="https://github.com/$REPO/releases/download/v${version}/${artifact}"
+
+ echo "EchoHub Installer"
+ echo " Version: v${version}"
+ echo " Platform: ${os}-${arch}"
+ echo " Install to: ${install_dir}"
+ echo ""
+
+ # Create temp directory with cleanup trap
+ tmpdir=$(mktemp -d)
+ trap 'rm -rf "$tmpdir"' EXIT
+
+ echo "Downloading ${artifact}..."
+ download "$url" "$tmpdir/echohub.zip"
+
+ echo "Extracting..."
+ if command -v unzip >/dev/null 2>&1; then
+ unzip -qo "$tmpdir/echohub.zip" -d "$tmpdir/extract"
+ else
+ echo "Error: unzip is required to extract the archive." >&2
+ echo "Install it with: apt install unzip / brew install unzip" >&2
+ exit 1
+ fi
+
+ # The ZIP contains a client-{os}-{arch}/ subdirectory
+ src_dir="$tmpdir/extract/client-${os}-${arch}"
+ if [ ! -d "$src_dir" ]; then
+ # Fallback: look for any directory containing the binary
+ src_dir=$(find "$tmpdir/extract" -name "EchoHub.Client" -type f -printf '%h' -quit 2>/dev/null || true)
+ if [ -z "$src_dir" ]; then
+ echo "Error: Could not find EchoHub.Client binary in the archive." >&2
+ exit 1
+ fi
+ fi
+
+ # Install the binary
+ mkdir -p "$install_dir"
+ cp "$src_dir/EchoHub.Client" "$install_dir/$BINARY_NAME"
+ chmod +x "$install_dir/$BINARY_NAME"
+
+ echo ""
+
+ # Ensure install directory is on PATH
+ if ! command -v "$BINARY_NAME" >/dev/null 2>&1; then
+ add_to_path "$install_dir"
+ fi
+
+ echo "Installed successfully! Run 'echohub' to start."
+ echo ""
+ if ! echo "$PATH" | tr ':' '\n' | grep -qx "$install_dir"; then
+ echo "NOTE: Restart your shell or run the following to use echohub now:"
+ echo ""
+ echo " export PATH=\"${install_dir}:\$PATH\""
+ fi
+}
+
+main
diff --git a/src/EchoHub.Client/Program.cs b/src/EchoHub.Client/Program.cs
index 1394ab2..f86f1c3 100644
--- a/src/EchoHub.Client/Program.cs
+++ b/src/EchoHub.Client/Program.cs
@@ -77,6 +77,9 @@ Log.Logger = new LoggerConfiguration()
Log.Information("EchoHub client starting");
+// == Ensure echohub is on PATH for convenient terminal access ============
+PathSetup.EnsureOnPath();
+
// == Post-update detection: stale backup cleanup or flag for rollback menu ================
if (UpdateBackupService.BackupExists())
{
diff --git a/src/EchoHub.Client/Services/PathSetup.cs b/src/EchoHub.Client/Services/PathSetup.cs
new file mode 100644
index 0000000..c7184d5
--- /dev/null
+++ b/src/EchoHub.Client/Services/PathSetup.cs
@@ -0,0 +1,102 @@
+using Serilog;
+
+namespace EchoHub.Client.Services;
+
+///
+/// Ensures the application's directory is on the system PATH so users
+/// can run 'echohub' from any terminal session.
+///
+public static class PathSetup
+{
+ private const string PathMarker = "# Added by EchoHub";
+
+ ///
+ /// Checks if the app directory is on PATH; if not, adds it persistently.
+ /// On Windows: modifies user-level PATH environment variable.
+ /// On Linux/macOS: appends an export line to shell profile files.
+ ///
+ public static void EnsureOnPath()
+ {
+ try
+ {
+ var appDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+
+ if (IsOnPath(appDir))
+ return;
+
+ if (OperatingSystem.IsWindows())
+ AddToWindowsPath(appDir);
+ else
+ AddToUnixPath(appDir);
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Could not add app directory to PATH");
+ }
+ }
+
+ private static bool IsOnPath(string directory)
+ {
+ var pathVar = Environment.GetEnvironmentVariable("PATH") ?? "";
+ var separator = OperatingSystem.IsWindows() ? ';' : ':';
+
+ return pathVar
+ .Split(separator, StringSplitOptions.RemoveEmptyEntries)
+ .Any(p => string.Equals(
+ p.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
+ directory,
+ OperatingSystem.IsWindows()
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal));
+ }
+
+ private static void AddToWindowsPath(string directory)
+ {
+ var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? "";
+
+ // Double-check against user PATH specifically (process PATH includes system + user)
+ if (userPath.Split(';', StringSplitOptions.RemoveEmptyEntries)
+ .Any(p => string.Equals(p.TrimEnd('\\', '/'), directory, StringComparison.OrdinalIgnoreCase)))
+ return;
+
+ var newPath = string.IsNullOrEmpty(userPath) ? directory : userPath + ";" + directory;
+ Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.User);
+ Log.Information("Added {Directory} to user PATH", directory);
+ }
+
+ private static void AddToUnixPath(string directory)
+ {
+ var exportLine = $"export PATH=\"{directory}:$PATH\" {PathMarker}";
+ var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+ // Target the most common shell profiles
+ string[] profiles = [
+ Path.Combine(home, ".profile"),
+ Path.Combine(home, ".bashrc"),
+ Path.Combine(home, ".zshrc")
+ ];
+
+ var added = false;
+ foreach (var profile in profiles)
+ {
+ if (!File.Exists(profile))
+ continue;
+
+ var content = File.ReadAllText(profile);
+ if (content.Contains(directory))
+ continue; // Already present (manual or previous run)
+
+ File.AppendAllText(profile, $"\n{exportLine}\n");
+ added = true;
+ Log.Information("Added PATH export to {Profile}", profile);
+ }
+
+ // If no profile existed, create .profile
+ if (!added)
+ {
+ var fallback = Path.Combine(home, ".profile");
+ File.AppendAllText(fallback, $"\n{exportLine}\n");
+ Log.Information("Created PATH export in {Profile}", fallback);
+ }
+ }
+}