12 Commits
1.0 ... 1.0.5

Author SHA1 Message Date
f916ba0059 fix version check 2025-07-07 11:32:38 -07:00
970d8c654f add version check 2025-07-07 11:02:07 -07:00
a5b3719d33 version preupdate 2025-07-07 10:29:23 -07:00
1200a1bfc2 updated to v2 api 2025-07-07 10:01:38 -07:00
af8a2dbd1e warning msg, readme 2025-07-04 10:24:23 -07:00
dd8282623e update README 2025-07-04 06:55:29 -07:00
a9367763f3 Merge pull request 'cleanup prompting' (#1) from cleanup into master
Reviewed-on: #1
2025-07-04 13:29:56 +00:00
7dd915b060 cleanup prompting 2025-07-04 06:10:25 -07:00
4e78277f99 Update README.md 2025-06-27 21:35:16 +00:00
ad97817e80 Update README.md 2025-06-27 21:05:46 +00:00
9a0606ce32 Update README.md 2025-06-27 21:04:45 +00:00
d4f841f8a0 update release script 2025-06-27 13:59:48 -07:00
7 changed files with 146 additions and 81 deletions

View File

@ -2,20 +2,18 @@
CLI tool for updating and launching [Project Epoch](https://www.project-epoch.net/) on Windows, Linux and macOS. CLI tool for updating and launching [Project Epoch](https://www.project-epoch.net/) on Windows, Linux and macOS.
## Setup ## Installing
### Windows
Download and extract the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page or build from source yourself, then copy `epochcli.exe` to a location of your choice. To make it easy, just use the same folder as your Wow game files.
### Linux ### Linux
A `wine` prefix with `dxvk` installed is sufficient, or you can use something like Lutris or faugus-launcher and just use `epochcli` for updating. A `wine` prefix with `dxvk` installed is sufficient, or you can use something like Lutris or faugus-launcher and just use `epochcli` for updating.
Download and extract the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page, build from source yourself, or use homebrew to install. Download and extract the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page, build from source yourself, or use homebrew from the macOS instructions to install.
If you are an Arch Linux (or derivative) user, you can use the provided [AUR package](https://aur.archlinux.org/packages/epochcli) to install
### macOS ### macOS
For macOS, I've found the best way to run Wow is in a Parallels Win 11 VM. Kegworks, Codeweavers, etc crash when the game starts up and I have not found a good solution so far. Any suggestions would be welcome, see my contact information below. For macOS, I've found the best way to run Wow is in a Parallels Win 11 VM. Kegworks, Codeweavers, etc crash when the game starts up and I have not found a good solution so far. Any suggestions would be welcome, see my contact information below. I currently use a Parallels VM and run the Windows version of epochcli inside the VM as an updater and launcher.
You can easily install with homebrew or build from source yourself. I dont have time to setup codesigning right now so there are no binaries provided for macOS. For homebrew, do the following: You can easily install with homebrew or build from source yourself. I dont have time to setup codesigning right now so there are no binaries provided for macOS. For homebrew, do the following:
@ -27,10 +25,16 @@ brew install --HEAD epochcli
brew upgrade epochcli --fetch-HEAD brew upgrade epochcli --fetch-HEAD
``` ```
## First Run ### Windows
Download and extract the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page or build from source yourself, then copy `epochcli.exe` to a location of your choice. To make it easy, just use the same folder as your Wow game files.
## Setup
![ss](ss.png)
1. Run `epochcli`. You will be taken through a setup process that configures the program and creates a config file at `$HOME/.config/epochcli/config.toml` 1. Run `epochcli`. You will be taken through a setup process that configures the program and creates a config file at `$HOME/.config/epochcli/config.toml`
2. You can now use `epochcli` as just a standalone updater or also a launcher based on your configuration. You can always run `epochcli -c` to redo the configuration or edit the config file manually 2. You can now use `epochcli` as a standalone updater, but it can also act as a launcher based on your configuration. You can always run `epochcli -c` to redo the configuration, or edit the config file manually
## Usage ## Usage
``` ```
@ -43,4 +47,4 @@ brew upgrade epochcli --fetch-HEAD
## Issues ## Issues
If you have any issues, [email me](mailto:evan@burkey.co) or ping `Battlehammer` on the Epoch discord If you have any issues, [email me](mailto:epochcli@burkey.co) or send a private message to `Battlehammer` on the Epoch discord

View File

@ -38,7 +38,7 @@ func setupConfig(rerun bool) (*Config, error) {
_, statErr := os.Stat(cfgPath) _, statErr := os.Stat(cfgPath)
if rerun || os.IsNotExist(statErr) { if rerun || os.IsNotExist(statErr) {
fmt.Println("Enter the path to your Wow directory below:") fmt.Println("Enter the path to your Wow directory below. Use the full path without shortcuts like '~' (ex: /home/user/epoch):")
var s string var s string
_, err = fmt.Scanln(&s) _, err = fmt.Scanln(&s)
if err != nil { if err != nil {
@ -46,29 +46,23 @@ func setupConfig(rerun bool) (*Config, error) {
} }
newConfig.WowDir = strings.TrimSpace(s) newConfig.WowDir = strings.TrimSpace(s)
for { p, err := promptYesNo(fmt.Sprintf("Do you want to use epochcli to launch Wow? Select No if you plan on using a launcher tool like Lutris (y/n): "))
fmt.Printf("Do you want to use epochcli to launch Wow? Select No if you plan on using a launcher tool like Lutris (y/n): ")
_, err = fmt.Scanf("%s", &s)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read input: %v", err) return nil, err
} }
s = strings.TrimSpace(s) if p {
if s == "y" || s == "Y" {
newConfig.EnableLauncher = true newConfig.EnableLauncher = true
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
newConfig.LaunchCmd = path.Join(newConfig.WowDir, "Project-Epoch.exe") newConfig.LaunchCmd = path.Join(newConfig.WowDir, "Project-Epoch.exe")
exePath, err := os.Executable() exePath, err := os.Executable()
if err != nil { if err != nil {
fmt.Println("unable to create desktop shortcut: ", err) return nil, fmt.Errorf("unable to create desktop shortcut: %v", err)
break
} }
err = makeLink(exePath, path.Join(home, "Desktop", "Project-Epoch.lnk")) err = makeLink(exePath, path.Join(home, "Desktop", "Project-Epoch.lnk"))
if err != nil { if err != nil {
fmt.Println("unable to create desktop shortcut: ", err) return nil, fmt.Errorf("unable to create desktop shortcut: %v", err)
} }
break
} else { } else {
fmt.Println("Enter your launch command to start Wow below. If you would rather configure this later in the configuration file, just press Enter") fmt.Println("Enter your launch command to start Wow below. If you would rather configure this later in the configuration file, just press Enter")
fmt.Printf("> ") fmt.Printf("> ")
@ -82,18 +76,9 @@ func setupConfig(rerun bool) (*Config, error) {
if s != "" { if s != "" {
newConfig.LaunchCmd = s newConfig.LaunchCmd = s
} }
break
} }
} }
if s == "n" || s == "N" {
break
}
fmt.Println("Please enter a valid value of either 'y' or 'n'")
}
err = os.MkdirAll(filepath.Join(home, ".config", configDirName), 0755) err = os.MkdirAll(filepath.Join(home, ".config", configDirName), 0755)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create config directory: %v", err) return nil, fmt.Errorf("unable to create config directory: %v", err)
@ -120,3 +105,24 @@ func setupConfig(rerun bool) (*Config, error) {
return &newConfig, nil return &newConfig, nil
} }
func promptYesNo(prompt string) (bool, error) {
var s string
for {
fmt.Print(prompt)
_, err := fmt.Scanf("%s", &s)
if err != nil {
return false, fmt.Errorf("unable to read input: %v", err)
}
s := strings.TrimSpace(s)
if s == "y" || s == "Y" {
return true, nil
}
if s == "n" || s == "N" {
return false, nil
}
fmt.Println("Please enter a valid value of either 'y' or 'n'")
}
}

20
main.go
View File

@ -16,12 +16,22 @@ import (
) )
const ( const (
manifestUrl = "https://updater.project-epoch.net/api/manifest" manifestUrl = "https://updater.project-epoch.net/api/v2/manifest"
defaultWowPath = "/path/to/wow" defaultWowPath = "/path/to/wow"
defaultLaunchCmd = "not configured" defaultLaunchCmd = "not configured"
) )
func main() { func main() {
outOfDate, err := needUpdate()
if err != nil {
log.Fatal(err)
}
if outOfDate {
fmt.Println("There is a new version of epochcli, please update before running")
os.Exit(1)
}
var ( var (
helpFlag bool helpFlag bool
updateOnlyFlag bool updateOnlyFlag bool
@ -125,13 +135,14 @@ func downloadUpdate(config *Config, force bool) (DownloadStats, error) {
} }
defer outFile.Close() defer outFile.Close()
resp, err := http.Get(file.URL) for _, url := range []string{file.Urls.Cloudflare, file.Urls.Digitalocean, file.Urls.None} {
resp, err := http.Get(url)
if err != nil { if err != nil {
return stats, err return stats, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return stats, fmt.Errorf("failed to download update from %s, status code: %d", file.URL, resp.StatusCode) return stats, fmt.Errorf("failed to download update from %s, status code: %d", url, resp.StatusCode)
} }
_, err = io.Copy(outFile, resp.Body) _, err = io.Copy(outFile, resp.Body)
@ -139,6 +150,9 @@ func downloadUpdate(config *Config, force bool) (DownloadStats, error) {
return stats, err return stats, err
} }
break
}
stats.updated += 1 stats.updated += 1
} }

View File

@ -11,13 +11,18 @@ type File struct {
Hash string `json:"Hash"` Hash string `json:"Hash"`
Size int `json:"Size"` Size int `json:"Size"`
Custom bool `json:"Custom"` Custom bool `json:"Custom"`
URL string `json:"URL"` Urls struct {
Origin string `json:"Origin"` Digitalocean string `json:"digitalocean"`
Cloudflare string `json:"cloudflare"`
None string `json:"none"`
} `json:"Urls"`
} }
type Manifest struct { type Manifest struct {
Version string `json:"Version"` Version string `json:"Version"`
UID string `json:"Uid"`
Files []File `json:"Files"` Files []File `json:"Files"`
CheckedAt string `json:"checked_at"`
} }
func getManifest() (*Manifest, error) { func getManifest() (*Manifest, error) {

View File

@ -3,20 +3,20 @@
set -e set -e
rm -f ./*.tar.gz rm -f ./*.tar.gz
rm -rf bin rm -f ./*.zip
mkdir bin GOOS=linux GOARCH=amd64 go build -o epochcli
tar czvf epochcli-linux-amd64.tar.gz epochcli
rm ./epochcli
GOOS=linux GOARCH=amd64 go build -o bin/epochcli-linux-amd64 GOOS=linux GOARCH=arm64 go build -o epochcli
tar czvf epochcli-linux-amd64.tar.gz bin/epochcli-linux-amd64 tar czvf epochcli-linux-arm64.tar.gz epochcli
rm ./epochcli
GOOS=linux GOARCH=arm64 go build -o bin/epochcli-linux-arm64 GOOS=windows GOARCH=amd64 go build -o epochcli.exe
tar czvf epochcli-linux-arm64.tar.gz bin/epochcli-linux-arm64 zip -j epochcli-windows-amd64.zip epochcli.exe
rm ./epochcli.exe
GOOS=windows GOARCH=amd64 go build -o bin/epochcli-windows-amd64 GOOS=windows GOARCH=arm64 go build -o epochcli.exe
zip epochcli-windows-amd64.zip bin/epochcli-windows-amd64 zip -j epochcli-windows-arm64.zip epochcli.exe
rm ./epochcli.exe
GOOS=windows GOARCH=arm64 go build -o bin/epochcli-windows-arm64
zip epochcli-windows-arm64.zip bin/epochcli-windows-arm64
rm -rf bin

BIN
ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

36
version.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"fmt"
"io"
"net/http"
"regexp"
)
const version = "1.0.5"
const versionUrl = "https://git.burkey.co/eburk/epochcli/raw/branch/master/version.go"
func needUpdate() (bool, error) {
resp, err := http.Get(versionUrl)
if err != nil {
return false, fmt.Errorf("unable to get version file: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("failed to download update, status code: %d", resp.StatusCode)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("unable to read response body: %v", err)
}
re := regexp.MustCompile(`const version = "([\d.]+)"`)
ver := re.FindStringSubmatch(string(b))
if len(ver) < 2 || ver[1] == "" {
return false, fmt.Errorf("unable to find version in response")
}
return ver[1] != version, nil
}