feat: parallel apply view (#47)

* feat: parallel apply view

* chore: cleaner readme

* chore: lint

* chore: remove debug logging

* chore: lint
This commit is contained in:
Vilsol 2023-12-13 15:34:01 -08:00 committed by GitHub
parent 98b7c99e74
commit b6592fe185
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 575 additions and 369 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

BIN
.github/screenshot.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -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

1
.gitignore vendored
View file

@ -128,3 +128,4 @@ dist/
/.graphqlconfig
schema.graphql
*.log
.direnv

View file

@ -50,7 +50,6 @@ linters:
- contextcheck
- durationcheck
- errorlint
- goconst
- goimports
- revive
- misspell

View file

@ -1,7 +1,11 @@
<img align="right" width="310" src="./.github/screenshot.png" />
# 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
<table>
@ -49,6 +53,14 @@ A CLI tool for managing mods for the game Satisfactory
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_armv7.apk">armv7</a></td>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_ppc64le.apk">ppc64le</a></td>
</tr>
<tr>
<th>Linux</th>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_amd64">amd64</a></td>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_386">386</a></td>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_arm64">arm64</a></td>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_armv7">armv7</a></td>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_ppc64le">ppc64le</a></td>
</tr>
<tr>
<th>macOS</th>
<td colspan="4" style="text-align: center"><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_darwin_all">darwin_all</a></td>
@ -56,7 +68,6 @@ A CLI tool for managing mods for the game Satisfactory
</tr>
</table>
## Usage
### Interactive CLI

View file

@ -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) {

View file

@ -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,
}
}

View file

@ -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()

View file

@ -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) {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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) {

View file

@ -163,49 +163,43 @@ 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{
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
Id: "Eqgr4VcB8y1z9a",
ID: "7QcfNdo5QAAyoC",
Version: "3.2.13",
Link: "/v1/version/Eqgr4VcB8y1z9a/download",
Hash: "8cabf9245e3f2a01b95cd3d39d98e407cfeccf355c19f1538fcbf868f81de008",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "ModularUI",
ModID: "ModularUI",
Condition: "^2.1.11",
Optional: false,
},
{
Mod_id: "RefinedRDLib",
ModID: "RefinedRDLib",
Condition: "^1.1.7",
Optional: false,
},
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
@ -213,23 +207,21 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "BwVKMJNP8doDLg",
ID: "7QcfNdo5QAAyoC",
Version: "3.2.11",
Link: "/v1/version/BwVKMJNP8doDLg/download",
Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "ModularUI",
ModID: "ModularUI",
Condition: "^2.1.10",
Optional: false,
},
{
Mod_id: "RefinedRDLib",
ModID: "RefinedRDLib",
Condition: "^1.1.6",
Optional: false,
},
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -237,23 +229,21 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "4XTjMpqFngbu9r",
ID: "7QcfNdo5QAAyoC",
Version: "3.2.10",
Link: "/v1/version/4XTjMpqFngbu9r/download",
Hash: "093f92c6d52c853bade386d5bc79cf103b27fb6e9d6f806850929b866ff98222",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "ModularUI",
ModID: "ModularUI",
Condition: "^2.1.9",
Optional: false,
},
{
Mod_id: "RefinedRDLib",
ModID: "RefinedRDLib",
Condition: "^1.1.5",
Optional: false,
},
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -261,21 +251,17 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
},
},
}, nil
case "AreaActions":
return &ficsit.ModVersionsWithDependenciesResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{
Id: "6vQ6ckVYFiidDh",
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
Id: "5KMXBkdAz5YJe",
ID: "7QcfNdo5QAAyoC",
Version: "1.6.7",
Link: "/v1/version/5KMXBkdAz5YJe/download",
Hash: "0baa673eea245b8ec5fe203a70b98deb666d85e27fb6ce9201e3c0fa3aaedcbe",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.4.1",
Optional: false,
},
@ -283,13 +269,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "EtEbwJj3smMn3o",
ID: "7QcfNdo5QAAyoC",
Version: "1.6.6",
Link: "/v1/version/EtEbwJj3smMn3o/download",
Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.2.0",
Optional: false,
},
@ -297,13 +281,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "9uw1eDwgrQs279",
ID: "7QcfNdo5QAAyoC",
Version: "1.6.5",
Link: "/v1/version/9uw1eDwgrQs279/download",
Hash: "427a93383fe8a8557096666b7e81bf5fb25f54a5428248904f52adc4dc34d60c",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.0.0",
Optional: false,
},
@ -311,21 +293,17 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
},
},
}, nil
case "RefinedRDLib":
return &ficsit.ModVersionsWithDependenciesResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{
Id: "B24emzbs6xVZQr",
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
Id: "2XcE6RUzGhZW7p",
ID: "7QcfNdo5QAAyoC",
Version: "1.1.7",
Link: "/v1/version/2XcE6RUzGhZW7p/download",
Hash: "034f3a7862d0153768e1a95d29d47a9d08ebcb7ff0fc8f9f2cb59147b09f16dd",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
@ -333,13 +311,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "52RMLEigqT5Ksn",
ID: "7QcfNdo5QAAyoC",
Version: "1.1.6",
Link: "/v1/version/52RMLEigqT5Ksn/download",
Hash: "9577e401e1a12a29657c8e3ed0cff34815009504dc62fc1a335b1e7a3b6fed12",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -347,13 +323,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "F4HY9eP4D5XjWQ",
ID: "7QcfNdo5QAAyoC",
Version: "1.1.5",
Link: "/v1/version/F4HY9eP4D5XjWQ/download",
Hash: "9cbeae078e28a661ebe15642e6d8f652c6c40c50dabd79a0781e25b84ed9bddf",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -361,21 +335,17 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
},
},
}, nil
case "ModularUI":
return &ficsit.ModVersionsWithDependenciesResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{
Id: "As2uJmQLLxjXLG",
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
Id: "7ay11W9MAv6MHs",
ID: "7QcfNdo5QAAyoC",
Version: "2.1.12",
Link: "/v1/version/7ay11W9MAv6MHs/download",
Hash: "a0de64c02448f9e37903e7569cc6ceee67f8e018f2774aac9cf295704b9e4696",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
@ -383,13 +353,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "4YuL9UbCDdzm68",
ID: "7QcfNdo5QAAyoC",
Version: "2.1.11",
Link: "/v1/version/4YuL9UbCDdzm68/download",
Hash: "b70658bfa74c132530046bee886c3c0f0277b95339b4fc67da6207cbd2cd422d",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -397,13 +365,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "5yY2zmx5nTyhWv",
ID: "7QcfNdo5QAAyoC",
Version: "2.1.10",
Link: "/v1/version/5yY2zmx5nTyhWv/download",
Hash: "7c523c9e6263a0b182ed42fe4d4de40aada10c17b1b344219618cd39055870bd",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -411,23 +377,25 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
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{
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
Id: "7ay11W9MAv6MHs",
ID: "7QcfNdo5QAAyoC",
Version: "0.10.1",
Link: "/v1/version/9LguyCdDUrpT9N/download",
Hash: "9278b37653ad33dd859875929b15cd1f8aba88d0ea65879df2db1ae8808029d4",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
@ -435,13 +403,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "DYvfwan5tYqZKE",
ID: "7QcfNdo5QAAyoC",
Version: "0.10.0",
Link: "/v1/version/DYvfwan5tYqZKE/download",
Hash: "8666b37b24188c3f56b1dad6f1d437c1127280381172a1046e85142e7cb81c64",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.5.0",
Optional: false,
},
@ -449,13 +415,11 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
{
Id: "918KMrX94xFpVw",
ID: "7QcfNdo5QAAyoC",
Version: "0.9.8",
Link: "/v1/version/918KMrX94xFpVw/download",
Hash: "d4fed641b6ecb25b9191f4dd7210576e9bd7bc644abcb3ca592200ccfd08fc44",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{
Dependencies: []ficsit.Dependency{
{
Mod_id: "SML",
ModID: "SML",
Condition: "^3.4.1",
Optional: false,
},
@ -463,7 +427,6 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
Targets: commonTargets,
},
},
},
}, nil
}

33
ficsit/rest.go Normal file
View file

@ -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
}

32
ficsit/types_rest.go Normal file
View file

@ -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"`
}

75
flake.lock Normal file
View file

@ -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
}

19
flake.nix Normal file
View file

@ -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; };
}
);
}

2
go.mod
View file

@ -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

8
shell.nix Normal file
View file

@ -0,0 +1,8 @@
{ pkgs, unstable }:
pkgs.mkShell {
nativeBuildInputs = with pkgs.buildPackages; [
unstable.go_1_21
unstable.golangci-lint
];
}

View file

@ -2,6 +2,7 @@ package scenes
import (
"sort"
"sync"
"github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea"
@ -35,24 +36,29 @@ type apply struct {
root components.RootModel
parent tea.Model
error *components.ErrorComponent
installChannel chan string
updateChannel chan cli.InstallUpdate
updateChannel chan applyUpdate
doneChannel chan bool
errorChannel chan error
cancelChannel chan bool
title string
status status
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)
@ -63,26 +69,34 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
overall: overall,
sub: sub,
status: status{
installName: "",
done: false,
},
installChannel: installChannel,
status: make(map[string]status),
updateChannel: updateChannel,
doneChannel: doneChannel,
errorChannel: errorChannel,
cancelChannel: cancelChannel,
cancelled: false,
}
go func() {
var wg sync.WaitGroup
for _, installation := range root.GetGlobal().Installations.Installations {
installChannel <- installation.Path
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:
}
if stop {
break
}
updateChannel <- applyUpdate{
Installation: installation,
Done: true,
}
}(installation)
}
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 {
s := m.status[update.Installation.Path]
if update.Done {
s.done = true
} else {
switch update.Update.Type {
case cli.InstallUpdateTypeOverall:
m.status.overallProgress = update.Progress
s.overallProgress = update.Update.Progress
case cli.InstallUpdateTypeModDownload:
m.status.modProgresses[update.Item.Mod] = modProgress{
downloadProgress: update.Progress,
s.modProgresses[update.Update.Item.Mod] = modProgress{
downloadProgress: update.Update.Progress,
downloading: true,
complete: false,
}
case cli.InstallUpdateTypeModExtract:
m.status.modProgresses[update.Item.Mod] = modProgress{
extractProgress: update.Progress,
s.modProgresses[update.Update.Item.Mod] = modProgress{
extractProgress: update.Update.Progress,
downloading: false,
complete: false,
}
case cli.InstallUpdateTypeModComplete:
m.status.modProgresses[update.Item.Mod] = modProgress{
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++
}
sort.Strings(installationList)
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 m.status.modProgresses {
for k := range s.modProgresses {
modReferences = append(modReferences, k)
}
sort.Strings(modReferences)
if m.root.Size().Height > totalHeight {
for _, modReference := range modReferences {
p := m.status.modProgresses[modReference]
if p.complete {
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.NewStyle().Render(modReference+" (Downloading)"))
strs = append(strs, m.sub.ViewAs(p.downloadProgress.Percentage()))
strs = append(strs, lipgloss.JoinHorizontal(
lipgloss.Left,
m.sub.ViewAs(p.downloadProgress.Percentage()),
" - ",
lipgloss.NewStyle().Render(modReference+" (Downloading)"),
))
} else {
strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Extracting)"))
strs = append(strs, m.sub.ViewAs(p.extractProgress.Percentage()))
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 {

View file

@ -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())
}