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 - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.21 go-version: 1.21.4
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -33,7 +33,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.21 go-version: 1.21.4
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -47,7 +47,7 @@ jobs:
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:
version: v1.54 version: v1.55.2
skip-pkg-cache: true skip-pkg-cache: true
skip-build-cache: true skip-build-cache: true
@ -62,7 +62,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.21 go-version: 1.21.4
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2

1
.gitignore vendored
View file

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

View file

@ -50,7 +50,6 @@ linters:
- contextcheck - contextcheck
- durationcheck - durationcheck
- errorlint - errorlint
- goconst
- goimports - goimports
- revive - revive
- misspell - 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) # 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 A CLI tool for managing mods for the game Satisfactory
---
## Installation ## Installation
<table> <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_armv7.apk">armv7</a></td>
<td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_ppc64le.apk">ppc64le</a></td> <td><a href="https://github.com/Vilsol/ficsit-cli/releases/latest/download/ficsit_linux_ppc64le.apk">ppc64le</a></td>
</tr> </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> <tr>
<th>macOS</th> <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> <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> </tr>
</table> </table>
## Usage ## Usage
### Interactive CLI ### 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) { 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") downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
if err := os.MkdirAll(downloadCache, 0o777); err != nil { if err := os.MkdirAll(downloadCache, 0o777); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {

View file

@ -34,7 +34,7 @@ type ficsitAPISource struct {
provider provider.Provider provider provider.Provider
lockfile *LockFile lockfile *LockFile
toInstall map[string]semver.Constraint toInstall map[string]semver.Constraint
modVersionInfo *xsync.MapOf[string, ficsit.ModVersionsWithDependenciesResponse] modVersionInfo *xsync.MapOf[string, ficsit.AllVersionsResponse]
gameVersion semver.Version gameVersion semver.Version
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
} }
@ -70,12 +70,15 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg) 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) return nil, errors.Errorf("mod %s not found", pkg)
} }
f.modVersionInfo.Store(pkg, *response) f.modVersionInfo.Store(pkg, *response)
versions := make([]pubgrub.PackageVersion, len(response.Mod.Versions)) versions := make([]pubgrub.PackageVersion, len(response.Data))
for i, modVersion := range response.Mod.Versions { for i, modVersion := range response.Data {
v, err := semver.NewVersion(modVersion.Version) v, err := semver.NewVersion(modVersion.Version)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to parse version %s", modVersion.Version) 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) return nil, errors.Wrapf(err, "failed to parse constraint %s", dependency.Condition)
} }
if dependency.Optional { if dependency.Optional {
optionalDependencies[dependency.Mod_id] = c optionalDependencies[dependency.ModID] = c
} else { } else {
dependencies[dependency.Mod_id] = c dependencies[dependency.ModID] = c
} }
} }
versions[i] = pubgrub.PackageVersion{ versions[i] = pubgrub.PackageVersion{
@ -144,7 +147,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
gameVersion: gameVersionSemver, gameVersion: gameVersionSemver,
lockfile: lockFile, lockfile: lockFile,
toInstall: toInstall, toInstall: toInstall,
modVersionInfo: xsync.NewMapOf[string, ficsit.ModVersionsWithDependenciesResponse](), modVersionInfo: xsync.NewMapOf[string, ficsit.AllVersionsResponse](),
} }
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg) 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) value, _ := ficsitSource.modVersionInfo.Load(k)
versions := value.Mod.Versions versions := value.Data
for _, ver := range versions { for _, ver := range versions {
if ver.Version == v.RawString() { if ver.Version == v.RawString() {
targets := make(map[string]LockedModTarget) targets := make(map[string]LockedModTarget)
for _, target := range ver.Targets { for _, target := range ver.Targets {
targets[string(target.TargetName)] = LockedModTarget{ targets[target.TargetName] = LockedModTarget{
Link: viper.GetString("api-base") + target.Link, Link: viper.GetString("api-base") + "/v1/version/" + ver.ID + "/" + target.TargetName + "/download",
Hash: target.Hash, Hash: target.Hash,
} }
} }

View file

@ -588,6 +588,9 @@ func downloadAndExtractMod(modReference string, version string, link string, has
} }
if updates != nil { if updates != nil {
close(downloadUpdates)
close(extractUpdates)
updates <- InstallUpdate{ updates <- InstallUpdate{
Type: InstallUpdateTypeModComplete, Type: InstallUpdateTypeModComplete,
Item: InstallUpdateItem{ Item: InstallUpdateItem{
@ -595,8 +598,6 @@ func downloadAndExtractMod(modReference string, version string, link string, has
Version: version, Version: version,
}, },
} }
close(extractUpdates)
} }
wg.Wait() wg.Wait()

View file

@ -34,8 +34,8 @@ func (p ficsitProvider) SMLVersions(context context.Context) (*ficsit.SMLVersion
return ficsit.SMLVersions(context, p.client) return ficsit.SMLVersions(context, p.client)
} }
func (p ficsitProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) { func (p ficsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
return ficsit.ModVersionsWithDependencies(context, p.client, modID) return ficsit.GetAllModVersions(modID)
} }
func (p ficsitProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) { 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 }, 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) cachedModFiles, err := cache.GetCacheMod(modID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get cache") return nil, errors.Wrap(err, "failed to get cache")
} }
versions := make([]ficsit.ModVersionsWithDependenciesModVersionsVersion, 0) versions := make([]ficsit.ModVersion, 0)
for _, modFile := range cachedModFiles { for _, modFile := range cachedModFiles {
versions = append(versions, ficsit.ModVersionsWithDependenciesModVersionsVersion{ versions = append(versions, ficsit.ModVersion{
Id: modID + ":" + modFile.Plugin.SemVersion, ID: modID + ":" + modFile.Plugin.SemVersion,
Version: modFile.Plugin.SemVersion, Version: modFile.Plugin.SemVersion,
}) })
} }
return &ficsit.ModVersionsWithDependenciesResponse{ return &ficsit.AllVersionsResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{ Success: true,
Id: modID, Data: versions,
Versions: versions,
},
}, nil }, nil
} }

View file

@ -50,7 +50,7 @@ func (p MixedProvider) SMLVersions(context context.Context) (*ficsit.SMLVersions
return p.ficsitProvider.SMLVersions(context) 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 { if p.Offline {
return p.localProvider.ModVersionsWithDependencies(context, modID) return p.localProvider.ModVersionsWithDependencies(context, modID)
} }

View file

@ -11,7 +11,7 @@ type Provider interface {
GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error)
ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error)
SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, 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) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error)
IsOffline() bool IsOffline() bool
} }

View file

@ -86,7 +86,7 @@ func TestResolutionNonExistentMod(t *testing.T) {
}, },
}).Resolve(resolver, nil, math.MaxInt) }).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) { func TestUpdateMods(t *testing.T) {

View file

@ -163,305 +163,268 @@ func (m MockProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsRespons
}, nil }, nil
} }
var commonTargets = []ficsit.ModVersionsWithDependenciesModVersionsVersionTargetsVersionTarget{ var commonTargets = []ficsit.Target{
{ {
TargetName: ficsit.TargetNameWindows, TargetName: "Windows",
Link: "/v1/version/7QcfNdo5QAAyoC/Windows/download",
Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9", Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9",
}, },
{ {
TargetName: ficsit.TargetNameWindowsserver, TargetName: "WindowsServer",
Link: "/v1/version/7QcfNdo5QAAyoC/WindowsServer/download",
Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917", Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917",
}, },
{ {
TargetName: ficsit.TargetNameLinuxserver, TargetName: "LinuxServer",
Link: "/v1/version/7QcfNdo5QAAyoC/LinuxServer/download",
Hash: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85", 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 { switch modID {
case "RefinedPower": case "RefinedPower":
return &ficsit.ModVersionsWithDependenciesResponse{ return &ficsit.AllVersionsResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{ Success: true,
Id: "DGiLzB3ZErWu2V", Data: []ficsit.ModVersion{
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ {
{ ID: "7QcfNdo5QAAyoC",
Id: "Eqgr4VcB8y1z9a", Version: "3.2.13",
Version: "3.2.13", Dependencies: []ficsit.Dependency{
Link: "/v1/version/Eqgr4VcB8y1z9a/download", {
Hash: "8cabf9245e3f2a01b95cd3d39d98e407cfeccf355c19f1538fcbf868f81de008", ModID: "ModularUI",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Condition: "^2.1.11",
{ Optional: false,
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,
},
}, },
Targets: commonTargets, {
}, ModID: "RefinedRDLib",
{ Condition: "^1.1.7",
Id: "BwVKMJNP8doDLg", Optional: false,
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,
},
}, },
Targets: commonTargets, {
}, ModID: "SML",
{ Condition: "^3.6.1",
Id: "4XTjMpqFngbu9r", Optional: false,
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,
},
}, },
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 }, nil
case "AreaActions": case "AreaActions":
return &ficsit.ModVersionsWithDependenciesResponse{ return &ficsit.AllVersionsResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{ Success: true,
Id: "6vQ6ckVYFiidDh", Data: []ficsit.ModVersion{
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ {
{ ID: "7QcfNdo5QAAyoC",
Id: "5KMXBkdAz5YJe", Version: "1.6.7",
Version: "1.6.7", Dependencies: []ficsit.Dependency{
Link: "/v1/version/5KMXBkdAz5YJe/download", {
Hash: "0baa673eea245b8ec5fe203a70b98deb666d85e27fb6ce9201e3c0fa3aaedcbe", ModID: "SML",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Condition: "^3.4.1",
{ Optional: false,
Mod_id: "SML",
Condition: "^3.4.1",
Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "EtEbwJj3smMn3o", },
Version: "1.6.6", {
Link: "/v1/version/EtEbwJj3smMn3o/download", ID: "7QcfNdo5QAAyoC",
Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89", Version: "1.6.6",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.2.0", Condition: "^3.2.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "9uw1eDwgrQs279", },
Version: "1.6.5", {
Link: "/v1/version/9uw1eDwgrQs279/download", ID: "7QcfNdo5QAAyoC",
Hash: "427a93383fe8a8557096666b7e81bf5fb25f54a5428248904f52adc4dc34d60c", Version: "1.6.5",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.0.0", Condition: "^3.0.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
Targets: commonTargets,
}, },
}, },
}, nil }, nil
case "RefinedRDLib": case "RefinedRDLib":
return &ficsit.ModVersionsWithDependenciesResponse{ return &ficsit.AllVersionsResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{ Success: true,
Id: "B24emzbs6xVZQr", Data: []ficsit.ModVersion{
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ {
{ ID: "7QcfNdo5QAAyoC",
Id: "2XcE6RUzGhZW7p", Version: "1.1.7",
Version: "1.1.7", Dependencies: []ficsit.Dependency{
Link: "/v1/version/2XcE6RUzGhZW7p/download", {
Hash: "034f3a7862d0153768e1a95d29d47a9d08ebcb7ff0fc8f9f2cb59147b09f16dd", ModID: "SML",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Condition: "^3.6.1",
{ Optional: false,
Mod_id: "SML",
Condition: "^3.6.1",
Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "52RMLEigqT5Ksn", },
Version: "1.1.6", {
Link: "/v1/version/52RMLEigqT5Ksn/download", ID: "7QcfNdo5QAAyoC",
Hash: "9577e401e1a12a29657c8e3ed0cff34815009504dc62fc1a335b1e7a3b6fed12", Version: "1.1.6",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.6.0", Condition: "^3.6.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "F4HY9eP4D5XjWQ", },
Version: "1.1.5", {
Link: "/v1/version/F4HY9eP4D5XjWQ/download", ID: "7QcfNdo5QAAyoC",
Hash: "9cbeae078e28a661ebe15642e6d8f652c6c40c50dabd79a0781e25b84ed9bddf", Version: "1.1.5",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.6.0", Condition: "^3.6.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
Targets: commonTargets,
}, },
}, },
}, nil }, nil
case "ModularUI": case "ModularUI":
return &ficsit.ModVersionsWithDependenciesResponse{ return &ficsit.AllVersionsResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{ Success: true,
Id: "As2uJmQLLxjXLG", Data: []ficsit.ModVersion{
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ {
{ ID: "7QcfNdo5QAAyoC",
Id: "7ay11W9MAv6MHs", Version: "2.1.12",
Version: "2.1.12", Dependencies: []ficsit.Dependency{
Link: "/v1/version/7ay11W9MAv6MHs/download", {
Hash: "a0de64c02448f9e37903e7569cc6ceee67f8e018f2774aac9cf295704b9e4696", ModID: "SML",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Condition: "^3.6.1",
{ Optional: false,
Mod_id: "SML",
Condition: "^3.6.1",
Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "4YuL9UbCDdzm68", },
Version: "2.1.11", {
Link: "/v1/version/4YuL9UbCDdzm68/download", ID: "7QcfNdo5QAAyoC",
Hash: "b70658bfa74c132530046bee886c3c0f0277b95339b4fc67da6207cbd2cd422d", Version: "2.1.11",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.6.0", Condition: "^3.6.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "5yY2zmx5nTyhWv", },
Version: "2.1.10", {
Link: "/v1/version/5yY2zmx5nTyhWv/download", ID: "7QcfNdo5QAAyoC",
Hash: "7c523c9e6263a0b182ed42fe4d4de40aada10c17b1b344219618cd39055870bd", Version: "2.1.10",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.6.0", Condition: "^3.6.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
Targets: commonTargets,
}, },
}, },
}, nil }, nil
case "ThisModDoesNotExist$$$": case "ThisModDoesNotExist$$$":
return &ficsit.ModVersionsWithDependenciesResponse{}, nil return &ficsit.AllVersionsResponse{
Success: false,
Error: &ficsit.Error{
Message: "mod not found",
Code: 200,
},
}, nil
case "FicsitRemoteMonitoring": case "FicsitRemoteMonitoring":
return &ficsit.ModVersionsWithDependenciesResponse{ return &ficsit.AllVersionsResponse{
Mod: ficsit.ModVersionsWithDependenciesMod{ Success: true,
Id: "9LguyCdDUrpT9N", Data: []ficsit.ModVersion{
Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ {
{ ID: "7QcfNdo5QAAyoC",
Id: "7ay11W9MAv6MHs", Version: "0.10.1",
Version: "0.10.1", Dependencies: []ficsit.Dependency{
Link: "/v1/version/9LguyCdDUrpT9N/download", {
Hash: "9278b37653ad33dd859875929b15cd1f8aba88d0ea65879df2db1ae8808029d4", ModID: "SML",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Condition: "^3.6.0",
{ Optional: false,
Mod_id: "SML",
Condition: "^3.6.0",
Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "DYvfwan5tYqZKE", },
Version: "0.10.0", {
Link: "/v1/version/DYvfwan5tYqZKE/download", ID: "7QcfNdo5QAAyoC",
Hash: "8666b37b24188c3f56b1dad6f1d437c1127280381172a1046e85142e7cb81c64", Version: "0.10.0",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.5.0", Condition: "^3.5.0",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
{ Targets: commonTargets,
Id: "918KMrX94xFpVw", },
Version: "0.9.8", {
Link: "/v1/version/918KMrX94xFpVw/download", ID: "7QcfNdo5QAAyoC",
Hash: "d4fed641b6ecb25b9191f4dd7210576e9bd7bc644abcb3ca592200ccfd08fc44", Version: "0.9.8",
Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ Dependencies: []ficsit.Dependency{
{ {
Mod_id: "SML", ModID: "SML",
Condition: "^3.4.1", Condition: "^3.4.1",
Optional: false, Optional: false,
},
}, },
Targets: commonTargets,
}, },
Targets: commonTargets,
}, },
}, },
}, nil }, 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 go 1.21
toolchain go1.21.4
require ( require (
github.com/JohannesKaufmann/html-to-markdown v1.4.2 github.com/JohannesKaufmann/html-to-markdown v1.4.2
github.com/Khan/genqlient v0.6.0 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 ( import (
"sort" "sort"
"sync"
"github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@ -32,57 +33,70 @@ type status struct {
} }
type apply struct { type apply struct {
root components.RootModel root components.RootModel
parent tea.Model parent tea.Model
error *components.ErrorComponent error *components.ErrorComponent
installChannel chan string updateChannel chan applyUpdate
updateChannel chan cli.InstallUpdate doneChannel chan bool
doneChannel chan bool errorChannel chan error
errorChannel chan error cancelChannel chan bool
cancelChannel chan bool title string
title string status map[string]status
status status overall progress.Model
overall progress.Model sub progress.Model
sub progress.Model cancelled bool
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 { func NewApply(root components.RootModel, parent tea.Model) tea.Model {
overall := progress.New(progress.WithSolidFill("118")) overall := progress.New(progress.WithSolidFill("118"))
sub := progress.New(progress.WithSolidFill("202")) sub := progress.New(progress.WithSolidFill("202"))
installChannel := make(chan string) updateChannel := make(chan applyUpdate)
updateChannel := make(chan cli.InstallUpdate)
doneChannel := make(chan bool, 1) doneChannel := make(chan bool, 1)
errorChannel := make(chan error) errorChannel := make(chan error)
cancelChannel := make(chan bool, 1) cancelChannel := make(chan bool, 1)
model := &apply{ model := &apply{
root: root, root: root,
parent: parent, parent: parent,
title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"), title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
overall: overall, overall: overall,
sub: sub, sub: sub,
status: status{ status: make(map[string]status),
installName: "", updateChannel: updateChannel,
done: false, doneChannel: doneChannel,
}, errorChannel: errorChannel,
installChannel: installChannel, cancelChannel: cancelChannel,
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 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) installUpdateChannel := make(chan cli.InstallUpdate)
go func() { go func() {
for update := range installUpdateChannel { 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 return
} }
stop := false updateChannel <- applyUpdate{
select { Installation: installation,
case <-cancelChannel: Done: true,
stop = true
default:
} }
}(installation)
}
if stop { go func() {
break wg.Wait()
}
}
doneChannel <- true doneChannel <- true
}() }()
@ -122,6 +133,13 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case keys.KeyQ: case keys.KeyQ:
fallthrough fallthrough
case keys.KeyEscape: case keys.KeyEscape:
if m.done {
if m.parent != nil {
return m.parent, m.parent.Init()
}
return m, tea.Quit
}
m.cancelled = true m.cancelled = true
if m.error != nil { if m.error != nil {
@ -134,7 +152,7 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cancelChannel <- true m.cancelChannel <- true
return m, nil return m, nil
case keys.KeyEnter: case keys.KeyEnter:
if m.status.done || m.error != nil { if m.done || m.error != nil {
if m.parent != nil { if m.parent != nil {
return m.parent, m.parent.Init() return m.parent, m.parent.Init()
} }
@ -149,35 +167,37 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case teaUtils.TickMsg: case teaUtils.TickMsg:
select { select {
case <-m.doneChannel: case <-m.doneChannel:
m.status.done = true m.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{}
break break
case update := <-m.updateChannel: case update := <-m.updateChannel:
switch update.Type { s := m.status[update.Installation.Path]
case cli.InstallUpdateTypeOverall:
m.status.overallProgress = update.Progress if update.Done {
case cli.InstallUpdateTypeModDownload: s.done = true
m.status.modProgresses[update.Item.Mod] = modProgress{ } else {
downloadProgress: update.Progress, switch update.Update.Type {
downloading: true, case cli.InstallUpdateTypeOverall:
complete: false, s.overallProgress = update.Update.Progress
} case cli.InstallUpdateTypeModDownload:
case cli.InstallUpdateTypeModExtract: s.modProgresses[update.Update.Item.Mod] = modProgress{
m.status.modProgresses[update.Item.Mod] = modProgress{ downloadProgress: update.Update.Progress,
extractProgress: update.Progress, downloading: true,
downloading: false, complete: false,
complete: false, }
} case cli.InstallUpdateTypeModExtract:
case cli.InstallUpdateTypeModComplete: s.modProgresses[update.Update.Item.Mod] = modProgress{
m.status.modProgresses[update.Item.Mod] = modProgress{ extractProgress: update.Update.Progress,
complete: true, downloading: false,
complete: false,
}
case cli.InstallUpdateTypeModComplete:
s.modProgresses[update.Update.Item.Mod] = modProgress{
complete: true,
}
} }
} }
m.status[update.Installation.Path] = s
break break
case err := <-m.errorChannel: case err := <-m.errorChannel:
wrappedErrMessage := wrap.String(err.Error(), int(float64(m.root.Size().Width)*0.8)) 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 { func (m apply) View() string {
strs := make([]string, 0) strs := make([]string, 0)
if m.status.installName != "" { installationList := make([]string, len(m.status))
strs = append(strs, lipgloss.NewStyle().Render(m.status.installName)) i := 0
strs = append(strs, lipgloss.NewStyle().MarginBottom(1).Render(m.overall.ViewAs(m.status.overallProgress.Percentage()))) for key := range m.status {
installationList[i] = key
i++
} }
modReferences := make([]string, 0) sort.Strings(installationList)
for k := range m.status.modProgresses {
modReferences = append(modReferences, k)
}
sort.Strings(modReferences)
for _, modReference := range modReferences { totalHeight := 3 + 3 // Header + Footer
p := m.status.modProgresses[modReference] totalHeight += len(installationList) * 2 // Bottom Margin + Overall progress per-install
if p.complete {
strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference) bottomMargins := 1
} else { if m.root.Size().Height < totalHeight {
if p.downloading { bottomMargins = 0
strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Downloading)")) }
strs = append(strs, m.sub.ViewAs(p.downloadProgress.Percentage()))
} else { totalHeight += len(installationList) // Top margin
strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Extracting)"))
strs = append(strs, m.sub.ViewAs(p.extractProgress.Percentage())) 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 { if m.cancelled {
strs = append(strs, teaUtils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return")) strs = append(strs, teaUtils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return"))
} else { } else {

View file

@ -228,6 +228,6 @@ func (m mainMenu) View() string {
return lipgloss.JoinVertical(lipgloss.Left, header, err, m.list.View()) 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()) return lipgloss.JoinVertical(lipgloss.Left, header, m.list.View())
} }