diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..8392d15
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
\ No newline at end of file
diff --git a/.github/screenshot.png b/.github/screenshot.png
new file mode 100644
index 0000000..c55906c
Binary files /dev/null and b/.github/screenshot.png differ
diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml
index 337e276..aacc8a5 100644
--- a/.github/workflows/push.yaml
+++ b/.github/workflows/push.yaml
@@ -10,7 +10,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.21
+ go-version: 1.21.4
- name: Check out code into the Go module directory
uses: actions/checkout@v2
@@ -33,7 +33,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.21
+ go-version: 1.21.4
- name: Check out code into the Go module directory
uses: actions/checkout@v2
@@ -47,7 +47,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
- version: v1.54
+ version: v1.55.2
skip-pkg-cache: true
skip-build-cache: true
@@ -62,7 +62,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.21
+ go-version: 1.21.4
- name: Check out code into the Go module directory
uses: actions/checkout@v2
diff --git a/.gitignore b/.gitignore
index 59e018d..2544bdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,4 +127,5 @@ dist/
/testdata
/.graphqlconfig
schema.graphql
-*.log
\ No newline at end of file
+*.log
+.direnv
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
index 636e98a..891f76c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -50,7 +50,6 @@ linters:
- contextcheck
- durationcheck
- errorlint
- - goconst
- goimports
- revive
- misspell
diff --git a/README.md b/README.md
index eb3ca0f..d0a9800 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
+
+
# ficsit-cli [![push](https://github.com/Vilsol/ficsit-cli/actions/workflows/push.yaml/badge.svg)](https://github.com/Vilsol/ficsit-cli/actions/workflows/push.yaml) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/vilsol/ficsit-cli) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/vilsol/ficsit-cli) [![GitHub license](https://img.shields.io/github/license/Vilsol/ficsit-cli)](https://github.com/Vilsol/ficsit-cli/blob/master/LICENSE) ![GitHub all releases](https://img.shields.io/github/downloads/vilsol/ficsit-cli/total)
A CLI tool for managing mods for the game Satisfactory
+---
+
## Installation
-
## Usage
### Interactive CLI
diff --git a/cli/cache/download.go b/cli/cache/download.go
index 4f6774c..5219ee3 100644
--- a/cli/cache/download.go
+++ b/cli/cache/download.go
@@ -14,10 +14,6 @@ import (
)
func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (*os.File, int64, error) {
- if updates != nil {
- defer close(updates)
- }
-
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
if err := os.MkdirAll(downloadCache, 0o777); err != nil {
if !os.IsExist(err) {
diff --git a/cli/dependency_resolver.go b/cli/dependency_resolver.go
index a0b7c7a..c7765d4 100644
--- a/cli/dependency_resolver.go
+++ b/cli/dependency_resolver.go
@@ -34,7 +34,7 @@ type ficsitAPISource struct {
provider provider.Provider
lockfile *LockFile
toInstall map[string]semver.Constraint
- modVersionInfo *xsync.MapOf[string, ficsit.ModVersionsWithDependenciesResponse]
+ modVersionInfo *xsync.MapOf[string, ficsit.AllVersionsResponse]
gameVersion semver.Version
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
}
@@ -70,12 +70,15 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg)
}
- if response.Mod.Id == "" {
+ if !response.Success {
+ if response.Error != nil {
+ return nil, errors.Errorf("mod %s not found: %s", pkg, response.Error.Message)
+ }
return nil, errors.Errorf("mod %s not found", pkg)
}
f.modVersionInfo.Store(pkg, *response)
- versions := make([]pubgrub.PackageVersion, len(response.Mod.Versions))
- for i, modVersion := range response.Mod.Versions {
+ versions := make([]pubgrub.PackageVersion, len(response.Data))
+ for i, modVersion := range response.Data {
v, err := semver.NewVersion(modVersion.Version)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse version %s", modVersion.Version)
@@ -88,9 +91,9 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi
return nil, errors.Wrapf(err, "failed to parse constraint %s", dependency.Condition)
}
if dependency.Optional {
- optionalDependencies[dependency.Mod_id] = c
+ optionalDependencies[dependency.ModID] = c
} else {
- dependencies[dependency.Mod_id] = c
+ dependencies[dependency.ModID] = c
}
}
versions[i] = pubgrub.PackageVersion{
@@ -144,7 +147,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
gameVersion: gameVersionSemver,
lockfile: lockFile,
toInstall: toInstall,
- modVersionInfo: xsync.NewMapOf[string, ficsit.ModVersionsWithDependenciesResponse](),
+ modVersionInfo: xsync.NewMapOf[string, ficsit.AllVersionsResponse](),
}
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg)
@@ -182,13 +185,13 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
}
value, _ := ficsitSource.modVersionInfo.Load(k)
- versions := value.Mod.Versions
+ versions := value.Data
for _, ver := range versions {
if ver.Version == v.RawString() {
targets := make(map[string]LockedModTarget)
for _, target := range ver.Targets {
- targets[string(target.TargetName)] = LockedModTarget{
- Link: viper.GetString("api-base") + target.Link,
+ targets[target.TargetName] = LockedModTarget{
+ Link: viper.GetString("api-base") + "/v1/version/" + ver.ID + "/" + target.TargetName + "/download",
Hash: target.Hash,
}
}
diff --git a/cli/installations.go b/cli/installations.go
index 428545c..5690f71 100644
--- a/cli/installations.go
+++ b/cli/installations.go
@@ -588,6 +588,9 @@ func downloadAndExtractMod(modReference string, version string, link string, has
}
if updates != nil {
+ close(downloadUpdates)
+ close(extractUpdates)
+
updates <- InstallUpdate{
Type: InstallUpdateTypeModComplete,
Item: InstallUpdateItem{
@@ -595,8 +598,6 @@ func downloadAndExtractMod(modReference string, version string, link string, has
Version: version,
},
}
-
- close(extractUpdates)
}
wg.Wait()
diff --git a/cli/provider/ficsit.go b/cli/provider/ficsit.go
index fa95459..9f5ccf1 100644
--- a/cli/provider/ficsit.go
+++ b/cli/provider/ficsit.go
@@ -34,8 +34,8 @@ func (p ficsitProvider) SMLVersions(context context.Context) (*ficsit.SMLVersion
return ficsit.SMLVersions(context, p.client)
}
-func (p ficsitProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
- return ficsit.ModVersionsWithDependencies(context, p.client, modID)
+func (p ficsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
+ return ficsit.GetAllModVersions(modID)
}
func (p ficsitProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
diff --git a/cli/provider/local.go b/cli/provider/local.go
index 1423f5c..8bdeec1 100644
--- a/cli/provider/local.go
+++ b/cli/provider/local.go
@@ -176,26 +176,24 @@ func (p localProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsRespon
}, nil
}
-func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
+func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
cachedModFiles, err := cache.GetCacheMod(modID)
if err != nil {
return nil, errors.Wrap(err, "failed to get cache")
}
- versions := make([]ficsit.ModVersionsWithDependenciesModVersionsVersion, 0)
+ versions := make([]ficsit.ModVersion, 0)
for _, modFile := range cachedModFiles {
- versions = append(versions, ficsit.ModVersionsWithDependenciesModVersionsVersion{
- Id: modID + ":" + modFile.Plugin.SemVersion,
+ versions = append(versions, ficsit.ModVersion{
+ ID: modID + ":" + modFile.Plugin.SemVersion,
Version: modFile.Plugin.SemVersion,
})
}
- return &ficsit.ModVersionsWithDependenciesResponse{
- Mod: ficsit.ModVersionsWithDependenciesMod{
- Id: modID,
- Versions: versions,
- },
+ return &ficsit.AllVersionsResponse{
+ Success: true,
+ Data: versions,
}, nil
}
diff --git a/cli/provider/mixed.go b/cli/provider/mixed.go
index 0081f0c..00def81 100644
--- a/cli/provider/mixed.go
+++ b/cli/provider/mixed.go
@@ -50,7 +50,7 @@ func (p MixedProvider) SMLVersions(context context.Context) (*ficsit.SMLVersions
return p.ficsitProvider.SMLVersions(context)
}
-func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
+func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
if p.Offline {
return p.localProvider.ModVersionsWithDependencies(context, modID)
}
diff --git a/cli/provider/provider.go b/cli/provider/provider.go
index 1e211db..d0b22a4 100644
--- a/cli/provider/provider.go
+++ b/cli/provider/provider.go
@@ -11,7 +11,7 @@ type Provider interface {
GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error)
ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error)
SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error)
- ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error)
+ ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error)
GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error)
IsOffline() bool
}
diff --git a/cli/resolving_test.go b/cli/resolving_test.go
index 9f0bfbb..28963e5 100644
--- a/cli/resolving_test.go
+++ b/cli/resolving_test.go
@@ -86,7 +86,7 @@ func TestResolutionNonExistentMod(t *testing.T) {
},
}).Resolve(resolver, nil, math.MaxInt)
- testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: failed to make decision: failed to get package versions: mod ThisModDoesNotExist$$$ not found", err.Error())
+ testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: failed to make decision: failed to get package versions: mod ThisModDoesNotExist$$$ not found: mod not found", err.Error())
}
func TestUpdateMods(t *testing.T) {
diff --git a/cli/test_helpers.go b/cli/test_helpers.go
index 38101a0..e7d434e 100644
--- a/cli/test_helpers.go
+++ b/cli/test_helpers.go
@@ -163,305 +163,268 @@ func (m MockProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsRespons
}, nil
}
-var commonTargets = []ficsit.ModVersionsWithDependenciesModVersionsVersionTargetsVersionTarget{
+var commonTargets = []ficsit.Target{
{
- TargetName: ficsit.TargetNameWindows,
- Link: "/v1/version/7QcfNdo5QAAyoC/Windows/download",
+ TargetName: "Windows",
Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9",
},
{
- TargetName: ficsit.TargetNameWindowsserver,
- Link: "/v1/version/7QcfNdo5QAAyoC/WindowsServer/download",
+ TargetName: "WindowsServer",
Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917",
},
{
- TargetName: ficsit.TargetNameLinuxserver,
- Link: "/v1/version/7QcfNdo5QAAyoC/LinuxServer/download",
+ TargetName: "LinuxServer",
Hash: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85",
},
}
-func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
+func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
switch modID {
case "RefinedPower":
- return &ficsit.ModVersionsWithDependenciesResponse{
- Mod: ficsit.ModVersionsWithDependenciesMod{
- Id: "DGiLzB3ZErWu2V",
- Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
- {
- Id: "Eqgr4VcB8y1z9a",
- Version: "3.2.13",
- Link: "/v1/version/Eqgr4VcB8y1z9a/download",
- Hash: "8cabf9245e3f2a01b95cd3d39d98e407cfeccf355c19f1538fcbf868f81de008",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "ModularUI",
- Condition: "^2.1.11",
- Optional: false,
- },
- {
- Mod_id: "RefinedRDLib",
- Condition: "^1.1.7",
- Optional: false,
- },
- {
- Mod_id: "SML",
- Condition: "^3.6.1",
- Optional: false,
- },
+ return &ficsit.AllVersionsResponse{
+ Success: true,
+ Data: []ficsit.ModVersion{
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "3.2.13",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "ModularUI",
+ Condition: "^2.1.11",
+ Optional: false,
},
- Targets: commonTargets,
- },
- {
- Id: "BwVKMJNP8doDLg",
- Version: "3.2.11",
- Link: "/v1/version/BwVKMJNP8doDLg/download",
- Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "ModularUI",
- Condition: "^2.1.10",
- Optional: false,
- },
- {
- Mod_id: "RefinedRDLib",
- Condition: "^1.1.6",
- Optional: false,
- },
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ {
+ ModID: "RefinedRDLib",
+ Condition: "^1.1.7",
+ Optional: false,
},
- Targets: commonTargets,
- },
- {
- Id: "4XTjMpqFngbu9r",
- Version: "3.2.10",
- Link: "/v1/version/4XTjMpqFngbu9r/download",
- Hash: "093f92c6d52c853bade386d5bc79cf103b27fb6e9d6f806850929b866ff98222",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "ModularUI",
- Condition: "^2.1.9",
- Optional: false,
- },
- {
- Mod_id: "RefinedRDLib",
- Condition: "^1.1.5",
- Optional: false,
- },
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ {
+ ModID: "SML",
+ Condition: "^3.6.1",
+ Optional: false,
},
- Targets: commonTargets,
},
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "3.2.11",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "ModularUI",
+ Condition: "^2.1.10",
+ Optional: false,
+ },
+ {
+ ModID: "RefinedRDLib",
+ Condition: "^1.1.6",
+ Optional: false,
+ },
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
+ },
+ },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "3.2.10",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "ModularUI",
+ Condition: "^2.1.9",
+ Optional: false,
+ },
+ {
+ ModID: "RefinedRDLib",
+ Condition: "^1.1.5",
+ Optional: false,
+ },
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
+ },
+ },
+ Targets: commonTargets,
},
},
}, nil
case "AreaActions":
- return &ficsit.ModVersionsWithDependenciesResponse{
- Mod: ficsit.ModVersionsWithDependenciesMod{
- Id: "6vQ6ckVYFiidDh",
- Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
- {
- Id: "5KMXBkdAz5YJe",
- Version: "1.6.7",
- Link: "/v1/version/5KMXBkdAz5YJe/download",
- Hash: "0baa673eea245b8ec5fe203a70b98deb666d85e27fb6ce9201e3c0fa3aaedcbe",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.4.1",
- Optional: false,
- },
+ return &ficsit.AllVersionsResponse{
+ Success: true,
+ Data: []ficsit.ModVersion{
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "1.6.7",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.4.1",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "EtEbwJj3smMn3o",
- Version: "1.6.6",
- Link: "/v1/version/EtEbwJj3smMn3o/download",
- Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.2.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "1.6.6",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.2.0",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "9uw1eDwgrQs279",
- Version: "1.6.5",
- Link: "/v1/version/9uw1eDwgrQs279/download",
- Hash: "427a93383fe8a8557096666b7e81bf5fb25f54a5428248904f52adc4dc34d60c",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.0.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "1.6.5",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.0.0",
+ Optional: false,
},
- Targets: commonTargets,
},
+ Targets: commonTargets,
},
},
}, nil
case "RefinedRDLib":
- return &ficsit.ModVersionsWithDependenciesResponse{
- Mod: ficsit.ModVersionsWithDependenciesMod{
- Id: "B24emzbs6xVZQr",
- Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
- {
- Id: "2XcE6RUzGhZW7p",
- Version: "1.1.7",
- Link: "/v1/version/2XcE6RUzGhZW7p/download",
- Hash: "034f3a7862d0153768e1a95d29d47a9d08ebcb7ff0fc8f9f2cb59147b09f16dd",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.1",
- Optional: false,
- },
+ return &ficsit.AllVersionsResponse{
+ Success: true,
+ Data: []ficsit.ModVersion{
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "1.1.7",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.1",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "52RMLEigqT5Ksn",
- Version: "1.1.6",
- Link: "/v1/version/52RMLEigqT5Ksn/download",
- Hash: "9577e401e1a12a29657c8e3ed0cff34815009504dc62fc1a335b1e7a3b6fed12",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "1.1.6",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "F4HY9eP4D5XjWQ",
- Version: "1.1.5",
- Link: "/v1/version/F4HY9eP4D5XjWQ/download",
- Hash: "9cbeae078e28a661ebe15642e6d8f652c6c40c50dabd79a0781e25b84ed9bddf",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "1.1.5",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
},
- Targets: commonTargets,
},
+ Targets: commonTargets,
},
},
}, nil
case "ModularUI":
- return &ficsit.ModVersionsWithDependenciesResponse{
- Mod: ficsit.ModVersionsWithDependenciesMod{
- Id: "As2uJmQLLxjXLG",
- Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
- {
- Id: "7ay11W9MAv6MHs",
- Version: "2.1.12",
- Link: "/v1/version/7ay11W9MAv6MHs/download",
- Hash: "a0de64c02448f9e37903e7569cc6ceee67f8e018f2774aac9cf295704b9e4696",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.1",
- Optional: false,
- },
+ return &ficsit.AllVersionsResponse{
+ Success: true,
+ Data: []ficsit.ModVersion{
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "2.1.12",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.1",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "4YuL9UbCDdzm68",
- Version: "2.1.11",
- Link: "/v1/version/4YuL9UbCDdzm68/download",
- Hash: "b70658bfa74c132530046bee886c3c0f0277b95339b4fc67da6207cbd2cd422d",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "2.1.11",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "5yY2zmx5nTyhWv",
- Version: "2.1.10",
- Link: "/v1/version/5yY2zmx5nTyhWv/download",
- Hash: "7c523c9e6263a0b182ed42fe4d4de40aada10c17b1b344219618cd39055870bd",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "2.1.10",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
},
- Targets: commonTargets,
},
+ Targets: commonTargets,
},
},
}, nil
case "ThisModDoesNotExist$$$":
- return &ficsit.ModVersionsWithDependenciesResponse{}, nil
+ return &ficsit.AllVersionsResponse{
+ Success: false,
+ Error: &ficsit.Error{
+ Message: "mod not found",
+ Code: 200,
+ },
+ }, nil
case "FicsitRemoteMonitoring":
- return &ficsit.ModVersionsWithDependenciesResponse{
- Mod: ficsit.ModVersionsWithDependenciesMod{
- Id: "9LguyCdDUrpT9N",
- Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
- {
- Id: "7ay11W9MAv6MHs",
- Version: "0.10.1",
- Link: "/v1/version/9LguyCdDUrpT9N/download",
- Hash: "9278b37653ad33dd859875929b15cd1f8aba88d0ea65879df2db1ae8808029d4",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.6.0",
- Optional: false,
- },
+ return &ficsit.AllVersionsResponse{
+ Success: true,
+ Data: []ficsit.ModVersion{
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "0.10.1",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.6.0",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "DYvfwan5tYqZKE",
- Version: "0.10.0",
- Link: "/v1/version/DYvfwan5tYqZKE/download",
- Hash: "8666b37b24188c3f56b1dad6f1d437c1127280381172a1046e85142e7cb81c64",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.5.0",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "0.10.0",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.5.0",
+ Optional: false,
},
- Targets: commonTargets,
},
- {
- Id: "918KMrX94xFpVw",
- Version: "0.9.8",
- Link: "/v1/version/918KMrX94xFpVw/download",
- Hash: "d4fed641b6ecb25b9191f4dd7210576e9bd7bc644abcb3ca592200ccfd08fc44",
- Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
- {
- Mod_id: "SML",
- Condition: "^3.4.1",
- Optional: false,
- },
+ Targets: commonTargets,
+ },
+ {
+ ID: "7QcfNdo5QAAyoC",
+ Version: "0.9.8",
+ Dependencies: []ficsit.Dependency{
+ {
+ ModID: "SML",
+ Condition: "^3.4.1",
+ Optional: false,
},
- Targets: commonTargets,
},
+ Targets: commonTargets,
},
},
}, nil
diff --git a/ficsit/rest.go b/ficsit/rest.go
new file mode 100644
index 0000000..d7bbc1e
--- /dev/null
+++ b/ficsit/rest.go
@@ -0,0 +1,33 @@
+package ficsit
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/spf13/viper"
+)
+
+const allVersionEndpoint = `/v1/mod/%s/versions/all`
+
+func GetAllModVersions(modID string) (*AllVersionsResponse, error) {
+ response, err := http.DefaultClient.Get(viper.GetString("api-base") + fmt.Sprintf(allVersionEndpoint, modID))
+ if err != nil {
+ return nil, fmt.Errorf("failed fetching all versions: %w", err)
+ }
+
+ defer response.Body.Close()
+
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed reading response body: %w", err)
+ }
+
+ allVersions := AllVersionsResponse{}
+ if err := json.Unmarshal(body, &allVersions); err != nil {
+ return nil, fmt.Errorf("failed parsing json: %w", err)
+ }
+
+ return &allVersions, nil
+}
diff --git a/ficsit/types_rest.go b/ficsit/types_rest.go
new file mode 100644
index 0000000..f9c5337
--- /dev/null
+++ b/ficsit/types_rest.go
@@ -0,0 +1,32 @@
+package ficsit
+
+type AllVersionsResponse struct {
+ Error *Error `json:"error,omitempty"`
+ Data []ModVersion `json:"data,omitempty"`
+ Success bool `json:"success"`
+}
+
+type ModVersion struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Dependencies []Dependency `json:"dependencies"`
+ Targets []Target `json:"targets"`
+}
+
+type Dependency struct {
+ ModID string `json:"mod_id"`
+ Condition string `json:"condition"`
+ Optional bool `json:"optional"`
+}
+
+type Target struct {
+ VersionID string `json:"version_id"`
+ TargetName string `json:"target_name"`
+ Hash string `json:"hash"`
+ Size int64 `json:"size"`
+}
+
+type Error struct {
+ Message string `json:"message"`
+ Code int64 `json:"code"`
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..0c52fe0
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,75 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1694529238,
+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1700014976,
+ "narHash": "sha256-dSGpS2YeJrXW5aH9y7Abd235gGufY3RuZFth6vuyVtU=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "592047fc9e4f7b74a4dc85d1b9f5243dfe4899e3",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "type": "indirect"
+ }
+ },
+ "nixpkgs-unstable": {
+ "locked": {
+ "lastModified": 1701040486,
+ "narHash": "sha256-vawYwoHA5CwvjfqaT3A5CT9V36Eq43gxdwpux32Qkjw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "45827faa2132b8eade424f6bdd48d8828754341a",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "ref": "nixpkgs-unstable",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "nixpkgs-unstable": "nixpkgs-unstable"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..0de54ec
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,19 @@
+{
+ description = "smr-cli";
+
+ inputs = {
+ flake-utils.url = "github:numtide/flake-utils";
+ nixpkgs-unstable.url = "flake:nixpkgs/nixpkgs-unstable";
+ };
+
+ outputs = { self, nixpkgs, flake-utils, nixpkgs-unstable }:
+ flake-utils.lib.eachDefaultSystem
+ (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ unstable = nixpkgs-unstable.legacyPackages.${system}; in
+ {
+ devShells.default = import ./shell.nix { inherit pkgs unstable; };
+ }
+ );
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9ecf293..9fdd0cf 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,8 @@ module github.com/satisfactorymodding/ficsit-cli
go 1.21
+toolchain go1.21.4
+
require (
github.com/JohannesKaufmann/html-to-markdown v1.4.2
github.com/Khan/genqlient v0.6.0
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..5d349dc
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,8 @@
+{ pkgs, unstable }:
+
+pkgs.mkShell {
+ nativeBuildInputs = with pkgs.buildPackages; [
+ unstable.go_1_21
+ unstable.golangci-lint
+ ];
+}
diff --git a/tea/scenes/apply.go b/tea/scenes/apply.go
index eaac113..4f46559 100644
--- a/tea/scenes/apply.go
+++ b/tea/scenes/apply.go
@@ -2,6 +2,7 @@ package scenes
import (
"sort"
+ "sync"
"github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea"
@@ -32,57 +33,70 @@ type status struct {
}
type apply struct {
- root components.RootModel
- parent tea.Model
- error *components.ErrorComponent
- installChannel chan string
- updateChannel chan cli.InstallUpdate
- doneChannel chan bool
- errorChannel chan error
- cancelChannel chan bool
- title string
- status status
- overall progress.Model
- sub progress.Model
- cancelled bool
+ root components.RootModel
+ parent tea.Model
+ error *components.ErrorComponent
+ updateChannel chan applyUpdate
+ doneChannel chan bool
+ errorChannel chan error
+ cancelChannel chan bool
+ title string
+ status map[string]status
+ overall progress.Model
+ sub progress.Model
+ cancelled bool
+ done bool
+}
+
+type applyUpdate struct {
+ Installation *cli.Installation
+ Update cli.InstallUpdate
+ Done bool
}
func NewApply(root components.RootModel, parent tea.Model) tea.Model {
overall := progress.New(progress.WithSolidFill("118"))
sub := progress.New(progress.WithSolidFill("202"))
- installChannel := make(chan string)
- updateChannel := make(chan cli.InstallUpdate)
+ updateChannel := make(chan applyUpdate)
doneChannel := make(chan bool, 1)
errorChannel := make(chan error)
cancelChannel := make(chan bool, 1)
model := &apply{
- root: root,
- parent: parent,
- title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
- overall: overall,
- sub: sub,
- status: status{
- installName: "",
- done: false,
- },
- installChannel: installChannel,
- updateChannel: updateChannel,
- doneChannel: doneChannel,
- errorChannel: errorChannel,
- cancelChannel: cancelChannel,
- cancelled: false,
+ root: root,
+ parent: parent,
+ title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
+ overall: overall,
+ sub: sub,
+ status: make(map[string]status),
+ updateChannel: updateChannel,
+ doneChannel: doneChannel,
+ errorChannel: errorChannel,
+ cancelChannel: cancelChannel,
}
- go func() {
- for _, installation := range root.GetGlobal().Installations.Installations {
- installChannel <- installation.Path
+ var wg sync.WaitGroup
+
+ for _, installation := range root.GetGlobal().Installations.Installations {
+ wg.Add(1)
+
+ model.status[installation.Path] = status{
+ modProgresses: make(map[string]modProgress),
+ installName: installation.Path,
+ overallProgress: utils.GenericProgress{},
+ }
+
+ go func(installation *cli.Installation) {
+ defer wg.Done()
installUpdateChannel := make(chan cli.InstallUpdate)
go func() {
for update := range installUpdateChannel {
- updateChannel <- update
+ updateChannel <- applyUpdate{
+ Installation: installation,
+ Update: update,
+ }
}
}()
@@ -91,18 +105,15 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
return
}
- stop := false
- select {
- case <-cancelChannel:
- stop = true
- default:
+ updateChannel <- applyUpdate{
+ Installation: installation,
+ Done: true,
}
+ }(installation)
+ }
- if stop {
- break
- }
- }
-
+ go func() {
+ wg.Wait()
doneChannel <- true
}()
@@ -122,6 +133,13 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case keys.KeyQ:
fallthrough
case keys.KeyEscape:
+ if m.done {
+ if m.parent != nil {
+ return m.parent, m.parent.Init()
+ }
+ return m, tea.Quit
+ }
+
m.cancelled = true
if m.error != nil {
@@ -134,7 +152,7 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cancelChannel <- true
return m, nil
case keys.KeyEnter:
- if m.status.done || m.error != nil {
+ if m.done || m.error != nil {
if m.parent != nil {
return m.parent, m.parent.Init()
}
@@ -149,35 +167,37 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case teaUtils.TickMsg:
select {
case <-m.doneChannel:
- m.status.done = true
- m.status.installName = ""
- break
- case installName := <-m.installChannel:
- m.status.installName = installName
- m.status.modProgresses = make(map[string]modProgress)
- m.status.overallProgress = utils.GenericProgress{}
+ m.done = true
break
case update := <-m.updateChannel:
- switch update.Type {
- case cli.InstallUpdateTypeOverall:
- m.status.overallProgress = update.Progress
- case cli.InstallUpdateTypeModDownload:
- m.status.modProgresses[update.Item.Mod] = modProgress{
- downloadProgress: update.Progress,
- downloading: true,
- complete: false,
- }
- case cli.InstallUpdateTypeModExtract:
- m.status.modProgresses[update.Item.Mod] = modProgress{
- extractProgress: update.Progress,
- downloading: false,
- complete: false,
- }
- case cli.InstallUpdateTypeModComplete:
- m.status.modProgresses[update.Item.Mod] = modProgress{
- complete: true,
+ s := m.status[update.Installation.Path]
+
+ if update.Done {
+ s.done = true
+ } else {
+ switch update.Update.Type {
+ case cli.InstallUpdateTypeOverall:
+ s.overallProgress = update.Update.Progress
+ case cli.InstallUpdateTypeModDownload:
+ s.modProgresses[update.Update.Item.Mod] = modProgress{
+ downloadProgress: update.Update.Progress,
+ downloading: true,
+ complete: false,
+ }
+ case cli.InstallUpdateTypeModExtract:
+ s.modProgresses[update.Update.Item.Mod] = modProgress{
+ extractProgress: update.Update.Progress,
+ downloading: false,
+ complete: false,
+ }
+ case cli.InstallUpdateTypeModComplete:
+ s.modProgresses[update.Update.Item.Mod] = modProgress{
+ complete: true,
+ }
}
}
+
+ m.status[update.Installation.Path] = s
break
case err := <-m.errorChannel:
wrappedErrMessage := wrap.String(err.Error(), int(float64(m.root.Size().Width)*0.8))
@@ -197,33 +217,77 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m apply) View() string {
strs := make([]string, 0)
- if m.status.installName != "" {
- strs = append(strs, lipgloss.NewStyle().Render(m.status.installName))
- strs = append(strs, lipgloss.NewStyle().MarginBottom(1).Render(m.overall.ViewAs(m.status.overallProgress.Percentage())))
+ installationList := make([]string, len(m.status))
+ i := 0
+ for key := range m.status {
+ installationList[i] = key
+ i++
}
- modReferences := make([]string, 0)
- for k := range m.status.modProgresses {
- modReferences = append(modReferences, k)
- }
- sort.Strings(modReferences)
+ sort.Strings(installationList)
- for _, modReference := range modReferences {
- p := m.status.modProgresses[modReference]
- if p.complete {
- strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference)
- } else {
- if p.downloading {
- strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Downloading)"))
- strs = append(strs, m.sub.ViewAs(p.downloadProgress.Percentage()))
- } else {
- strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Extracting)"))
- strs = append(strs, m.sub.ViewAs(p.extractProgress.Percentage()))
+ totalHeight := 3 + 3 // Header + Footer
+ totalHeight += len(installationList) * 2 // Bottom Margin + Overall progress per-install
+
+ bottomMargins := 1
+ if m.root.Size().Height < totalHeight {
+ bottomMargins = 0
+ }
+
+ totalHeight += len(installationList) // Top margin
+
+ topMargins := 1
+ if m.root.Size().Height < totalHeight {
+ topMargins = 0
+ }
+
+ for _, installPath := range installationList {
+ totalHeight += len(m.status[installPath].modProgresses)
+ }
+
+ for _, installPath := range installationList {
+ s := m.status[installPath]
+
+ strs = append(strs, lipgloss.NewStyle().Margin(topMargins, 0, bottomMargins, 1).Render(lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ m.overall.ViewAs(s.overallProgress.Percentage()),
+ " - ",
+ lipgloss.NewStyle().Render(installPath),
+ )))
+
+ modReferences := make([]string, 0)
+ for k := range s.modProgresses {
+ modReferences = append(modReferences, k)
+ }
+ sort.Strings(modReferences)
+
+ if m.root.Size().Height > totalHeight {
+ for _, modReference := range modReferences {
+ p := s.modProgresses[modReference]
+ if p.complete || s.done {
+ strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference)
+ } else {
+ if p.downloading {
+ strs = append(strs, lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ m.sub.ViewAs(p.downloadProgress.Percentage()),
+ " - ",
+ lipgloss.NewStyle().Render(modReference+" (Downloading)"),
+ ))
+ } else {
+ strs = append(strs, lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ m.sub.ViewAs(p.extractProgress.Percentage()),
+ " - ",
+ lipgloss.NewStyle().Render(modReference+" (Extracting)"),
+ ))
+ }
+ }
}
}
}
- if m.status.done {
+ if m.done {
if m.cancelled {
strs = append(strs, teaUtils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return"))
} else {
diff --git a/tea/scenes/main_menu.go b/tea/scenes/main_menu.go
index 6de6792..21bf546 100644
--- a/tea/scenes/main_menu.go
+++ b/tea/scenes/main_menu.go
@@ -228,6 +228,6 @@ func (m mainMenu) View() string {
return lipgloss.JoinVertical(lipgloss.Left, header, err, m.list.View())
}
- m.list.SetSize(m.list.Width(), m.root.Size().Height-lipgloss.Height(header)-1)
+ m.list.SetSize(m.list.Width(), m.root.Size().Height-lipgloss.Height(header))
return lipgloss.JoinVertical(lipgloss.Left, header, m.list.View())
}