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

3
.gitignore vendored
View file

@ -127,4 +127,5 @@ dist/
/testdata
/.graphqlconfig
schema.graphql
*.log
*.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,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

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

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