From c772ba347332fd7230f91151e0dbfdd1ce78a13a Mon Sep 17 00:00:00 2001
From: comp500 <comp500@users.noreply.github.com>
Date: Tue, 17 Sep 2019 21:26:48 +0100
Subject: [PATCH] Add init command

---
 cmd/init.go | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 go.mod      |   1 +
 go.sum      |   2 +
 3 files changed, 349 insertions(+)
 create mode 100644 cmd/init.go

diff --git a/cmd/init.go b/cmd/init.go
new file mode 100644
index 0000000..03738d1
--- /dev/null
+++ b/cmd/init.go
@@ -0,0 +1,346 @@
+package cmd
+
+import (
+	"bufio"
+	"encoding/json"
+	"encoding/xml"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/comp500/packwiz/core"
+	"github.com/fatih/camelcase"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+// initCmd represents the init command
+var initCmd = &cobra.Command{
+	Use:   "init",
+	Short: "Initialise a packwiz modpack",
+	Args:  cobra.NoArgs,
+	Run: func(cmd *cobra.Command, args []string) {
+		_, err := os.Stat(viper.GetString("pack-file"))
+		if err == nil && !viper.GetBool("init.reinit") {
+			fmt.Println("Modpack metadata file already exists, use -r to override!")
+			os.Exit(1)
+		} else if err != nil && !os.IsNotExist(err) {
+			fmt.Printf("Error checking pack file: %s\n", err)
+			os.Exit(1)
+		}
+
+		name, err := cmd.Flags().GetString("name")
+		if err != nil || len(name) == 0 {
+			// Get current file directory name
+			wd, err := os.Getwd()
+			directoryName := "."
+			if err == nil {
+				directoryName = filepath.Base(wd)
+			}
+			if directoryName != "." && len(directoryName) > 0 {
+				// Turn directory name into a space-seperated proper name
+				name = strings.ReplaceAll(strings.ReplaceAll(strings.Join(camelcase.Split(directoryName), " "), " - ", " "), " _ ", " ")
+				fmt.Print("Modpack name [" + name + "]: ")
+			} else {
+				fmt.Print("Modpack name: ")
+			}
+			readName, err := bufio.NewReader(os.Stdin).ReadString('\n')
+			if err != nil {
+				fmt.Printf("Error reading input: %s\n", err)
+				os.Exit(1)
+			}
+			// Trims both CR and LF
+			readName = strings.TrimSpace(strings.TrimRight(readName, "\r\n"))
+			if len(readName) > 0 {
+				name = readName
+			}
+		}
+
+		mcVersions, err := getValidMCVersions()
+		if err != nil {
+			fmt.Printf("Failed to get latest minecraft versions: %s", err)
+			os.Exit(1)
+		}
+
+		mcVersion := viper.GetString("init.mc-version")
+		if len(mcVersion) == 0 {
+			var latestVersion string
+			if viper.GetBool("init.snapshot") {
+				latestVersion = mcVersions.Latest.Snapshot
+			} else {
+				latestVersion = mcVersions.Latest.Release
+			}
+			if viper.GetBool("init.latest") {
+				mcVersion = latestVersion
+			} else {
+				fmt.Print("Minecraft version [" + latestVersion + "]: ")
+				mcVersion, err = bufio.NewReader(os.Stdin).ReadString('\n')
+				if err != nil {
+					fmt.Printf("Error reading input: %s\n", err)
+					os.Exit(1)
+				}
+				// Trims both CR and LF
+				mcVersion = strings.TrimSpace(strings.TrimRight(mcVersion, "\r\n"))
+				if len(mcVersion) == 0 {
+					mcVersion = latestVersion
+				}
+			}
+		}
+		mcVersions.checkValid(mcVersion)
+
+		// TODO: minecraft modloader
+		modLoaderName := viper.GetString("init.modloader")
+		if len(modLoaderName) == 0 {
+			var defaultLoader string
+			if viper.GetBool("init.snapshot") {
+				defaultLoader = "fabric"
+			} else {
+				defaultLoader = "forge"
+			}
+			fmt.Print("Mod loader [" + defaultLoader + "]: ")
+			modLoaderName, err = bufio.NewReader(os.Stdin).ReadString('\n')
+			if err != nil {
+				fmt.Printf("Error reading input: %s\n", err)
+				os.Exit(1)
+			}
+			// Trims both CR and LF
+			modLoaderName = strings.ToLower(strings.TrimSpace(strings.TrimRight(modLoaderName, "\r\n")))
+			if len(modLoaderName) == 0 {
+				modLoaderName = defaultLoader
+			}
+		}
+		_, ok := modLoaders[modLoaderName]
+		if modLoaderName != "none" && !ok {
+			fmt.Println("Given mod loader is not supported! Use \"none\" to specify no modloader, or to configure one manually.")
+			fmt.Print("The following mod loaders are supported: ")
+			keys := make([]string, len(modLoaders))
+			i := 0
+			for k := range modLoaders {
+				keys[i] = k
+				i++
+			}
+			fmt.Println(strings.Join(keys, ", "))
+			os.Exit(1)
+		}
+
+		var modLoaderVersion string
+		if modLoaderName != "none" {
+			versions, latestVersion, err := modLoaders[modLoaderName](mcVersion)
+			modLoaderVersion = viper.GetString("init.modloader-version")
+			if len(modLoaderVersion) == 0 {
+				if viper.GetBool("init.modloader-latest") {
+					modLoaderVersion = latestVersion
+				} else {
+					fmt.Print("Mod loader version [" + latestVersion + "]: ")
+					modLoaderVersion, err = bufio.NewReader(os.Stdin).ReadString('\n')
+					if err != nil {
+						fmt.Printf("Error reading input: %s\n", err)
+						os.Exit(1)
+					}
+					// Trims both CR and LF
+					modLoaderVersion = strings.ToLower(strings.TrimSpace(strings.TrimRight(modLoaderVersion, "\r\n")))
+					if len(modLoaderVersion) == 0 {
+						modLoaderVersion = latestVersion
+					}
+				}
+			}
+			found := false
+			for _, v := range versions {
+				if modLoaderVersion == v {
+					found = true
+					break
+				}
+			}
+			if !found {
+				fmt.Println("Given mod loader version cannot be found!")
+				os.Exit(1)
+			}
+		}
+
+		indexFilePath := viper.GetString("init.index-file")
+		_, err = os.Stat(indexFilePath)
+		if os.IsNotExist(err) {
+			// Create file
+			err = ioutil.WriteFile(indexFilePath, []byte{}, 0644)
+			if err != nil {
+				fmt.Printf("Error creating index file: %s\n", err)
+				os.Exit(1)
+			}
+			fmt.Println(indexFilePath + " created!")
+		} else if err != nil {
+			fmt.Printf("Error checking index file: %s\n", err)
+			os.Exit(1)
+		}
+
+		// Create the pack
+		pack := core.Pack{
+			Name: name,
+			Index: struct {
+				File       string `toml:"file"`
+				HashFormat string `toml:"hash-format"`
+				Hash       string `toml:"hash"`
+			}{
+				File: indexFilePath,
+			},
+			Versions: map[string]string{
+				"minecraft": mcVersion,
+			},
+		}
+		if modLoaderName != "none" {
+			pack.Versions[modLoaderName] = modLoaderVersion
+		}
+
+		// Refresh the index and pack
+		index, err := pack.LoadIndex()
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		err = index.Refresh()
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		err = index.Write()
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		err = pack.UpdateIndexHash()
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		err = pack.Write()
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		fmt.Println(viper.GetString("pack-file") + " created!")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(initCmd)
+
+	initCmd.Flags().String("name", "", "The name of the modpack (omit to define interactively)")
+	initCmd.Flags().String("index-file", "index.toml", "The index file to use")
+	viper.BindPFlag("init.index-file", initCmd.Flags().Lookup("index-file"))
+	initCmd.Flags().String("mc-version", "", "The version of Minecraft to use (omit to define interactively)")
+	viper.BindPFlag("init.mc-version", initCmd.Flags().Lookup("mc-version"))
+	initCmd.Flags().BoolP("latest", "l", false, "Automatically select the latest version of Minecraft")
+	viper.BindPFlag("init.latest", initCmd.Flags().Lookup("latest"))
+	initCmd.Flags().BoolP("snapshot", "s", false, "Use the latest snapshot version with --latest")
+	viper.BindPFlag("init.snapshot", initCmd.Flags().Lookup("snapshot"))
+	initCmd.Flags().BoolP("reinit", "r", false, "Recreate the pack file if it already exists, rather than exiting")
+	viper.BindPFlag("init.reinit", initCmd.Flags().Lookup("reinit"))
+	initCmd.Flags().String("modloader", "", "The mod loader to use (omit to define interactively)")
+	viper.BindPFlag("init.modloader", initCmd.Flags().Lookup("modloader"))
+	initCmd.Flags().String("modloader-version", "", "The mod loader version to use (omit to define interactively)")
+	viper.BindPFlag("init.modloader-version", initCmd.Flags().Lookup("modloader-version"))
+	initCmd.Flags().BoolP("modloader-latest", "L", false, "Automatically select the latest version of the mod loader")
+	viper.BindPFlag("init.modloader-latest", initCmd.Flags().Lookup("modloader-latest"))
+}
+
+type mcVersionManifest struct {
+	Latest struct {
+		Release  string `json:"release"`
+		Snapshot string `json:"snapshot"`
+	} `json:"latest"`
+	Versions []struct {
+		ID          string    `json:"id"`
+		Type        string    `json:"type"`
+		URL         string    `json:"url"`
+		Time        time.Time `json:"time"`
+		ReleaseTime time.Time `json:"releaseTime"`
+	} `json:"versions"`
+}
+
+func (m mcVersionManifest) checkValid(version string) {
+	for _, v := range m.Versions {
+		if v.ID == version {
+			return
+		}
+	}
+	fmt.Println("Given version is not a valid Minecraft version!")
+	os.Exit(1)
+}
+
+func getValidMCVersions() (mcVersionManifest, error) {
+	res, err := http.Get("https://launchermeta.mojang.com/mc/game/version_manifest.json")
+	if err != nil {
+		return mcVersionManifest{}, err
+	}
+	dec := json.NewDecoder(res.Body)
+	out := mcVersionManifest{}
+	err = dec.Decode(&out)
+	if err != nil {
+		return mcVersionManifest{}, err
+	}
+	// Sort by newest to oldest
+	sort.Slice(out.Versions, func(i, j int) bool {
+		return out.Versions[i].ReleaseTime.Before(out.Versions[j].ReleaseTime)
+	})
+	return out, nil
+}
+
+type mavenMetadata struct {
+	XMLName    xml.Name `xml:"metadata"`
+	GroupID    string   `xml:"groupId"`
+	ArtifactID string   `xml:"artifactId"`
+	Versioning struct {
+		Release  string `xml:"release"`
+		Versions struct {
+			Version []string `xml:"version"`
+		} `xml:"versions"`
+		LastUpdated string `xml:"lastUpdated"`
+	} `xml:"versioning"`
+}
+
+// Gets a list of modloader versions and latest version for a given Minecraft version
+var modLoaders = map[string]func(mcVersion string) ([]string, string, error){
+	"fabric": func(mcVersion string) ([]string, string, error) {
+		res, err := http.Get("https://maven.fabricmc.net/net/fabricmc/fabric-loader/maven-metadata.xml")
+		if err != nil {
+			return []string{}, "", err
+		}
+		dec := xml.NewDecoder(res.Body)
+		out := mavenMetadata{}
+		err = dec.Decode(&out)
+		if err != nil {
+			return []string{}, "", err
+		}
+		return out.Versioning.Versions.Version, out.Versioning.Release, nil
+	},
+	"forge": func(mcVersion string) ([]string, string, error) {
+		res, err := http.Get("https://files.minecraftforge.net/maven/net/minecraftforge/forge/maven-metadata.xml")
+		if err != nil {
+			return []string{}, "", err
+		}
+		dec := xml.NewDecoder(res.Body)
+		out := mavenMetadata{}
+		err = dec.Decode(&out)
+		if err != nil {
+			return []string{}, "", err
+		}
+		allowedVersions := make([]string, 0, len(out.Versioning.Versions.Version))
+		for _, v := range out.Versioning.Versions.Version {
+			if strings.HasPrefix(v, mcVersion) {
+				allowedVersions = append(allowedVersions, v)
+			}
+		}
+		if len(allowedVersions) == 0 {
+			return []string{}, "", errors.New("no Forge versions available for this Minecraft version")
+		}
+		if strings.HasPrefix(out.Versioning.Release, mcVersion) {
+			return allowedVersions, out.Versioning.Release, nil
+		}
+		return allowedVersions, allowedVersions[len(allowedVersions)-1], nil
+	},
+}
diff --git a/go.mod b/go.mod
index 6dd0e5c..bedd69a 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
 	github.com/daviddengcn/go-colortext v0.0.0-20180409174941-186a3d44e920 // indirect
 	github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817
+	github.com/fatih/camelcase v1.0.0
 	github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450 // indirect
 	github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995 // indirect
 	github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e // indirect
diff --git a/go.sum b/go.sum
index 5ab7355..a5ac337 100644
--- a/go.sum
+++ b/go.sum
@@ -30,6 +30,8 @@ github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 h1:0nsrg//Dc
 github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817/go.mod h1:C/+sI4IFnEpCn6VQ3GIPEp+FrQnQw+YQP3+n+GdGq7o=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
+github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=