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