factorix 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/completion/_factorix.bash +202 -0
- data/completion/_factorix.fish +197 -0
- data/completion/_factorix.zsh +376 -0
- data/doc/factorix.1 +377 -0
- data/exe/factorix +20 -0
- data/lib/factorix/api/category.rb +69 -0
- data/lib/factorix/api/image.rb +35 -0
- data/lib/factorix/api/license.rb +71 -0
- data/lib/factorix/api/mod_download_api.rb +66 -0
- data/lib/factorix/api/mod_info.rb +166 -0
- data/lib/factorix/api/mod_management_api.rb +237 -0
- data/lib/factorix/api/mod_portal_api.rb +204 -0
- data/lib/factorix/api/release.rb +49 -0
- data/lib/factorix/api/tag.rb +95 -0
- data/lib/factorix/api.rb +7 -0
- data/lib/factorix/api_credential.rb +54 -0
- data/lib/factorix/application.rb +218 -0
- data/lib/factorix/cache/file_system.rb +307 -0
- data/lib/factorix/cli/commands/backup_support.rb +46 -0
- data/lib/factorix/cli/commands/base.rb +90 -0
- data/lib/factorix/cli/commands/cache/evict.rb +180 -0
- data/lib/factorix/cli/commands/cache/stat.rb +201 -0
- data/lib/factorix/cli/commands/command_wrapper.rb +71 -0
- data/lib/factorix/cli/commands/completion.rb +83 -0
- data/lib/factorix/cli/commands/confirmable.rb +53 -0
- data/lib/factorix/cli/commands/download_support.rb +123 -0
- data/lib/factorix/cli/commands/launch.rb +79 -0
- data/lib/factorix/cli/commands/man.rb +29 -0
- data/lib/factorix/cli/commands/mod/check.rb +99 -0
- data/lib/factorix/cli/commands/mod/disable.rb +188 -0
- data/lib/factorix/cli/commands/mod/download.rb +291 -0
- data/lib/factorix/cli/commands/mod/edit.rb +114 -0
- data/lib/factorix/cli/commands/mod/enable.rb +216 -0
- data/lib/factorix/cli/commands/mod/image/add.rb +47 -0
- data/lib/factorix/cli/commands/mod/image/edit.rb +41 -0
- data/lib/factorix/cli/commands/mod/image/list.rb +74 -0
- data/lib/factorix/cli/commands/mod/install.rb +443 -0
- data/lib/factorix/cli/commands/mod/list.rb +372 -0
- data/lib/factorix/cli/commands/mod/search.rb +134 -0
- data/lib/factorix/cli/commands/mod/settings/dump.rb +88 -0
- data/lib/factorix/cli/commands/mod/settings/restore.rb +101 -0
- data/lib/factorix/cli/commands/mod/show.rb +202 -0
- data/lib/factorix/cli/commands/mod/sync.rb +299 -0
- data/lib/factorix/cli/commands/mod/uninstall.rb +325 -0
- data/lib/factorix/cli/commands/mod/update.rb +222 -0
- data/lib/factorix/cli/commands/mod/upload.rb +90 -0
- data/lib/factorix/cli/commands/path.rb +79 -0
- data/lib/factorix/cli/commands/requires_game_stopped.rb +32 -0
- data/lib/factorix/cli/commands/version.rb +25 -0
- data/lib/factorix/cli.rb +42 -0
- data/lib/factorix/dependency/edge.rb +89 -0
- data/lib/factorix/dependency/entry.rb +124 -0
- data/lib/factorix/dependency/graph/builder.rb +108 -0
- data/lib/factorix/dependency/graph.rb +210 -0
- data/lib/factorix/dependency/list.rb +244 -0
- data/lib/factorix/dependency/mod_version_requirement.rb +73 -0
- data/lib/factorix/dependency/node.rb +60 -0
- data/lib/factorix/dependency/parser.rb +148 -0
- data/lib/factorix/dependency/validation_result.rb +138 -0
- data/lib/factorix/dependency/validator.rb +190 -0
- data/lib/factorix/errors.rb +112 -0
- data/lib/factorix/formatting.rb +56 -0
- data/lib/factorix/game_version.rb +98 -0
- data/lib/factorix/http/cache_decorator.rb +106 -0
- data/lib/factorix/http/cached_response.rb +37 -0
- data/lib/factorix/http/client.rb +187 -0
- data/lib/factorix/http/response.rb +31 -0
- data/lib/factorix/http/retry_decorator.rb +59 -0
- data/lib/factorix/http/retry_strategy.rb +80 -0
- data/lib/factorix/info_json.rb +90 -0
- data/lib/factorix/installed_mod.rb +239 -0
- data/lib/factorix/mod.rb +55 -0
- data/lib/factorix/mod_list.rb +174 -0
- data/lib/factorix/mod_settings.rb +278 -0
- data/lib/factorix/mod_state.rb +34 -0
- data/lib/factorix/mod_version.rb +99 -0
- data/lib/factorix/portal.rb +185 -0
- data/lib/factorix/progress/download_handler.rb +46 -0
- data/lib/factorix/progress/multi_presenter.rb +45 -0
- data/lib/factorix/progress/presenter.rb +67 -0
- data/lib/factorix/progress/presenter_adapter.rb +46 -0
- data/lib/factorix/progress/scan_handler.rb +33 -0
- data/lib/factorix/progress/upload_handler.rb +33 -0
- data/lib/factorix/runtime/base.rb +233 -0
- data/lib/factorix/runtime/linux.rb +32 -0
- data/lib/factorix/runtime/mac_os.rb +53 -0
- data/lib/factorix/runtime/user_configurable.rb +69 -0
- data/lib/factorix/runtime/windows.rb +85 -0
- data/lib/factorix/runtime/wsl.rb +118 -0
- data/lib/factorix/runtime.rb +32 -0
- data/lib/factorix/save_file.rb +178 -0
- data/lib/factorix/ser_des/deserializer.rb +198 -0
- data/lib/factorix/ser_des/serializer.rb +231 -0
- data/lib/factorix/ser_des/signed_integer.rb +63 -0
- data/lib/factorix/ser_des/unsigned_integer.rb +65 -0
- data/lib/factorix/service_credential.rb +127 -0
- data/lib/factorix/transfer/downloader.rb +162 -0
- data/lib/factorix/transfer/uploader.rb +232 -0
- data/lib/factorix/version.rb +6 -0
- data/lib/factorix.rb +38 -0
- data/sig/dry/auto_inject.rbs +15 -0
- data/sig/dry/cli.rbs +19 -0
- data/sig/dry/configurable.rbs +13 -0
- data/sig/dry/core/container.rbs +17 -0
- data/sig/dry/events/publisher.rbs +22 -0
- data/sig/dry/logger.rbs +16 -0
- data/sig/factorix/api/category.rbs +15 -0
- data/sig/factorix/api/image.rbs +15 -0
- data/sig/factorix/api/license.rbs +20 -0
- data/sig/factorix/api/mod_download_api.rbs +18 -0
- data/sig/factorix/api/mod_info.rbs +67 -0
- data/sig/factorix/api/mod_management_api.rbs +25 -0
- data/sig/factorix/api/mod_portal_api.rbs +31 -0
- data/sig/factorix/api/release.rbs +27 -0
- data/sig/factorix/api/tag.rbs +15 -0
- data/sig/factorix/api.rbs +8 -0
- data/sig/factorix/api_credential.rbs +17 -0
- data/sig/factorix/application.rbs +86 -0
- data/sig/factorix/cache/file_system.rbs +35 -0
- data/sig/factorix/cli/commands/base.rbs +13 -0
- data/sig/factorix/cli/commands/cache/evict.rbs +17 -0
- data/sig/factorix/cli/commands/cache/stat.rbs +17 -0
- data/sig/factorix/cli/commands/command_wrapper.rbs +13 -0
- data/sig/factorix/cli/commands/completion/zsh.rbs +15 -0
- data/sig/factorix/cli/commands/confirmable.rbs +12 -0
- data/sig/factorix/cli/commands/download_support.rbs +12 -0
- data/sig/factorix/cli/commands/launch.rbs +15 -0
- data/sig/factorix/cli/commands/mod/check.rbs +18 -0
- data/sig/factorix/cli/commands/mod/disable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/download.rbs +18 -0
- data/sig/factorix/cli/commands/mod/edit.rbs +30 -0
- data/sig/factorix/cli/commands/mod/enable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/image/add.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/edit.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/list.rbs +19 -0
- data/sig/factorix/cli/commands/mod/install.rbs +19 -0
- data/sig/factorix/cli/commands/mod/list.rbs +30 -0
- data/sig/factorix/cli/commands/mod/search.rbs +18 -0
- data/sig/factorix/cli/commands/mod/settings/dump.rbs +17 -0
- data/sig/factorix/cli/commands/mod/settings/restore.rbs +17 -0
- data/sig/factorix/cli/commands/mod/sync.rbs +19 -0
- data/sig/factorix/cli/commands/mod/uninstall.rbs +20 -0
- data/sig/factorix/cli/commands/mod/update.rbs +19 -0
- data/sig/factorix/cli/commands/mod/upload.rbs +24 -0
- data/sig/factorix/cli/commands/path.rbs +18 -0
- data/sig/factorix/cli/commands/requires_game_stopped.rbs +13 -0
- data/sig/factorix/cli/commands/version.rbs +13 -0
- data/sig/factorix/cli.rbs +11 -0
- data/sig/factorix/dependency/edge.rbs +32 -0
- data/sig/factorix/dependency/entry.rbs +30 -0
- data/sig/factorix/dependency/graph/builder.rbs +17 -0
- data/sig/factorix/dependency/graph.rbs +39 -0
- data/sig/factorix/dependency/list.rbs +69 -0
- data/sig/factorix/dependency/mod_version_requirement.rbs +18 -0
- data/sig/factorix/dependency/node.rbs +24 -0
- data/sig/factorix/dependency/parser.rbs +11 -0
- data/sig/factorix/dependency/validation_result.rbs +56 -0
- data/sig/factorix/dependency/validator.rbs +13 -0
- data/sig/factorix/errors.rbs +132 -0
- data/sig/factorix/formatting.rbs +8 -0
- data/sig/factorix/game_version.rbs +24 -0
- data/sig/factorix/http/cache_decorator.rbs +64 -0
- data/sig/factorix/http/client.rbs +55 -0
- data/sig/factorix/http/response.rbs +28 -0
- data/sig/factorix/http/retry_decorator.rbs +44 -0
- data/sig/factorix/http/retry_strategy.rbs +42 -0
- data/sig/factorix/info_json.rbs +19 -0
- data/sig/factorix/installed_mod.rbs +34 -0
- data/sig/factorix/mod.rbs +20 -0
- data/sig/factorix/mod_list.rbs +44 -0
- data/sig/factorix/mod_settings.rbs +47 -0
- data/sig/factorix/mod_state.rbs +18 -0
- data/sig/factorix/mod_version.rbs +23 -0
- data/sig/factorix/portal.rbs +37 -0
- data/sig/factorix/progress/download_handler.rbs +19 -0
- data/sig/factorix/progress/multi_presenter.rbs +15 -0
- data/sig/factorix/progress/presenter.rbs +17 -0
- data/sig/factorix/progress/presenter_adapter.rbs +17 -0
- data/sig/factorix/progress/scan_handler.rbs +16 -0
- data/sig/factorix/progress/upload_handler.rbs +17 -0
- data/sig/factorix/runtime/base.rbs +45 -0
- data/sig/factorix/runtime/linux.rbs +15 -0
- data/sig/factorix/runtime/mac_os.rbs +15 -0
- data/sig/factorix/runtime/user_configurable.rbs +13 -0
- data/sig/factorix/runtime/windows.rbs +23 -0
- data/sig/factorix/runtime/wsl.rbs +19 -0
- data/sig/factorix/runtime.rbs +9 -0
- data/sig/factorix/save_file.rbs +40 -0
- data/sig/factorix/ser_des/deserializer.rbs +49 -0
- data/sig/factorix/ser_des/serializer.rbs +45 -0
- data/sig/factorix/ser_des/signed_integer.rbs +37 -0
- data/sig/factorix/ser_des/unsigned_integer.rbs +37 -0
- data/sig/factorix/service_credential.rbs +19 -0
- data/sig/factorix/transfer/downloader.rbs +15 -0
- data/sig/factorix/transfer/uploader.rbs +21 -0
- data/sig/factorix.rbs +9 -0
- data/sig/tty/progressbar.rbs +18 -0
- metadata +431 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module MOD
|
|
7
|
+
# List installed MODs
|
|
8
|
+
class List < Base
|
|
9
|
+
# @!parse
|
|
10
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
11
|
+
# attr_reader :logger
|
|
12
|
+
# # @return [Factorix::Runtime]
|
|
13
|
+
# attr_reader :runtime
|
|
14
|
+
# # @return [Factorix::API::MODPortalAPI]
|
|
15
|
+
# attr_reader :mod_portal_api
|
|
16
|
+
include Import[:logger, :runtime, :mod_portal_api]
|
|
17
|
+
|
|
18
|
+
desc "List installed MOD(s)"
|
|
19
|
+
|
|
20
|
+
example [
|
|
21
|
+
" # List all installed MOD(s)",
|
|
22
|
+
"--enabled # List only enabled MOD(s)",
|
|
23
|
+
"--outdated # List MOD(s) with available updates",
|
|
24
|
+
"--json # Output in JSON format"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
option :enabled, type: :flag, default: false, desc: "Show only enabled MOD(s)"
|
|
28
|
+
option :disabled, type: :flag, default: false, desc: "Show only disabled MOD(s)"
|
|
29
|
+
option :errors, type: :flag, default: false, desc: "Show only MOD(s) with dependency errors"
|
|
30
|
+
option :outdated, type: :flag, default: false, desc: "Show only MOD(s) with available updates"
|
|
31
|
+
option :json, type: :flag, default: false, desc: "Output in JSON format"
|
|
32
|
+
|
|
33
|
+
MODInfo = Data.define(:name, :version, :enabled, :error, :latest_version)
|
|
34
|
+
|
|
35
|
+
# MOD information for display
|
|
36
|
+
#
|
|
37
|
+
# This class encapsulates MOD information for display purposes,
|
|
38
|
+
# including the MOD name, version, enabled status, error messages,
|
|
39
|
+
# and latest available version.
|
|
40
|
+
class MODInfo
|
|
41
|
+
# @!attribute [r] name
|
|
42
|
+
# @return [String] MOD name
|
|
43
|
+
# @!attribute [r] version
|
|
44
|
+
# @return [MODVersion] MOD version
|
|
45
|
+
# @!attribute [r] enabled
|
|
46
|
+
# @return [Boolean] enabled status
|
|
47
|
+
# @!attribute [r] error
|
|
48
|
+
# @return [String, nil] error message if any
|
|
49
|
+
# @!attribute [r] latest_version
|
|
50
|
+
# @return [MODVersion, nil] latest version available on portal
|
|
51
|
+
|
|
52
|
+
# Get the display status string
|
|
53
|
+
#
|
|
54
|
+
# @return [String] "error", "enabled", or "disabled"
|
|
55
|
+
def status
|
|
56
|
+
return "error" if error
|
|
57
|
+
|
|
58
|
+
enabled ? "enabled" : "disabled"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if a newer version is available
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] true if latest_version is newer than current version
|
|
64
|
+
def outdated?
|
|
65
|
+
return false unless latest_version
|
|
66
|
+
|
|
67
|
+
latest_version > version
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Execute the list command
|
|
72
|
+
#
|
|
73
|
+
# @param enabled [Boolean] show only enabled MODs
|
|
74
|
+
# @param disabled [Boolean] show only disabled MODs
|
|
75
|
+
# @param errors [Boolean] show only MODs with dependency errors
|
|
76
|
+
# @param outdated [Boolean] show only MODs with available updates
|
|
77
|
+
# @param json [Boolean] output in JSON format
|
|
78
|
+
# @return [void]
|
|
79
|
+
def call(enabled:, disabled:, errors:, outdated:, json:, **)
|
|
80
|
+
validate_filter_options!(enabled:, disabled:, errors:, outdated:)
|
|
81
|
+
|
|
82
|
+
mod_list = MODList.load
|
|
83
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: $stderr)
|
|
84
|
+
handler = Progress::ScanHandler.new(presenter)
|
|
85
|
+
installed_mods = InstalledMOD.all(handler:)
|
|
86
|
+
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
87
|
+
|
|
88
|
+
validator = Dependency::Validator.new(graph:, mod_list:, installed_mods:)
|
|
89
|
+
validation_result = validator.validate
|
|
90
|
+
|
|
91
|
+
mod_infos = build_mod_infos(installed_mods, mod_list, validation_result)
|
|
92
|
+
total_count = mod_infos.size
|
|
93
|
+
|
|
94
|
+
# Apply filters
|
|
95
|
+
mod_infos = apply_filters(mod_infos, enabled:, disabled:, errors:, outdated:)
|
|
96
|
+
|
|
97
|
+
# Sort
|
|
98
|
+
mod_infos = sort_mods(mod_infos)
|
|
99
|
+
|
|
100
|
+
# Determine active filter for summary
|
|
101
|
+
active_filter = if enabled then :enabled
|
|
102
|
+
elsif disabled then :disabled
|
|
103
|
+
elsif errors then :errors
|
|
104
|
+
elsif outdated then :outdated
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Output
|
|
108
|
+
if json
|
|
109
|
+
output_json(mod_infos)
|
|
110
|
+
else
|
|
111
|
+
output_table(mod_infos, show_latest: outdated, active_filter:, total_count:)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Validate that conflicting filter options are not specified together
|
|
116
|
+
#
|
|
117
|
+
# @param enabled [Boolean] show only enabled MODs
|
|
118
|
+
# @param disabled [Boolean] show only disabled MODs
|
|
119
|
+
# @param errors [Boolean] show only MODs with dependency errors
|
|
120
|
+
# @param outdated [Boolean] show only MODs with available updates
|
|
121
|
+
# @return [void]
|
|
122
|
+
# @raise [InvalidArgumentError] if conflicting options are specified
|
|
123
|
+
private def validate_filter_options!(enabled:, disabled:, errors:, outdated:)
|
|
124
|
+
filters = []
|
|
125
|
+
filters << "--enabled" if enabled
|
|
126
|
+
filters << "--disabled" if disabled
|
|
127
|
+
filters << "--errors" if errors
|
|
128
|
+
filters << "--outdated" if outdated
|
|
129
|
+
|
|
130
|
+
return if filters.size <= 1
|
|
131
|
+
|
|
132
|
+
raise InvalidArgumentError, "Cannot combine #{filters.join(", ")} options"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Build list of MOD info from installed MODs
|
|
136
|
+
#
|
|
137
|
+
# @param installed_mods [Array<InstalledMOD>] installed MODs
|
|
138
|
+
# @param mod_list [MODList] MOD list with enabled status
|
|
139
|
+
# @param validation_result [Dependency::ValidationResult] validation result with errors
|
|
140
|
+
# @return [Array<MODInfo>] MOD info list
|
|
141
|
+
private def build_mod_infos(installed_mods, mod_list, validation_result)
|
|
142
|
+
grouped = installed_mods.group_by(&:mod)
|
|
143
|
+
error_map = build_error_map(validation_result)
|
|
144
|
+
|
|
145
|
+
grouped.map {|mod, versions|
|
|
146
|
+
display_version = determine_display_version(mod, versions, mod_list)
|
|
147
|
+
enabled = mod_list.exist?(mod) && mod_list.enabled?(mod)
|
|
148
|
+
error = error_map[mod.name]
|
|
149
|
+
|
|
150
|
+
MODInfo.new(name: mod.name, version: display_version, enabled:, error:, latest_version: nil)
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Build a map of MOD name to error messages from validation result
|
|
155
|
+
#
|
|
156
|
+
# @param validation_result [Dependency::ValidationResult] validation result
|
|
157
|
+
# @return [Hash<String, String>] map of MOD name to error message
|
|
158
|
+
private def build_error_map(validation_result)
|
|
159
|
+
error_map = {}
|
|
160
|
+
|
|
161
|
+
validation_result.errors.each do |error|
|
|
162
|
+
mod = error.mod
|
|
163
|
+
next unless mod
|
|
164
|
+
|
|
165
|
+
# Use the first error for each MOD
|
|
166
|
+
error_map[mod.name] ||= error.message
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
error_map
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Determine which version of a MOD to display
|
|
173
|
+
#
|
|
174
|
+
# Returns the specified version from mod-list.json if present,
|
|
175
|
+
# otherwise returns the latest installed version.
|
|
176
|
+
#
|
|
177
|
+
# @param mod [MOD] the MOD
|
|
178
|
+
# @param versions [Array<InstalledMOD>] installed versions of the MOD
|
|
179
|
+
# @param mod_list [MODList] MOD list with enabled status and version
|
|
180
|
+
# @return [MODVersion] the version to display
|
|
181
|
+
private def determine_display_version(mod, versions, mod_list)
|
|
182
|
+
if mod_list.exist?(mod)
|
|
183
|
+
specified_version = mod_list.version(mod)
|
|
184
|
+
return specified_version if specified_version
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
versions.map(&:version).max
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Apply filters to MOD info list
|
|
191
|
+
#
|
|
192
|
+
# @param mod_infos [Array<MODInfo>] MOD info list
|
|
193
|
+
# @param enabled [Boolean] show only enabled MODs
|
|
194
|
+
# @param disabled [Boolean] show only disabled MODs
|
|
195
|
+
# @param errors [Boolean] show only MODs with dependency errors
|
|
196
|
+
# @param outdated [Boolean] show only MODs with available updates
|
|
197
|
+
# @return [Array<MODInfo>] filtered MOD info list
|
|
198
|
+
private def apply_filters(mod_infos, enabled:, disabled:, errors:, outdated:)
|
|
199
|
+
if enabled
|
|
200
|
+
mod_infos = mod_infos.select(&:enabled)
|
|
201
|
+
elsif disabled
|
|
202
|
+
mod_infos = mod_infos.reject(&:enabled)
|
|
203
|
+
elsif errors
|
|
204
|
+
mod_infos = mod_infos.select(&:error)
|
|
205
|
+
elsif outdated
|
|
206
|
+
mod_infos = fetch_latest_versions(mod_infos)
|
|
207
|
+
mod_infos = mod_infos.select(&:outdated?)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
mod_infos
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Default number of parallel jobs for fetching latest versions
|
|
214
|
+
DEFAULT_JOBS = 4
|
|
215
|
+
private_constant :DEFAULT_JOBS
|
|
216
|
+
|
|
217
|
+
# Fetch latest versions from portal for outdated check
|
|
218
|
+
#
|
|
219
|
+
# @param mod_infos [Array<MODInfo>] MOD info list
|
|
220
|
+
# @return [Array<MODInfo>] MOD info list with latest versions
|
|
221
|
+
private def fetch_latest_versions(mod_infos)
|
|
222
|
+
# Separate base/expansion from regular MODs
|
|
223
|
+
base_and_expansions, regular_mods = mod_infos.partition {|info|
|
|
224
|
+
mod = Factorix::MOD[name: info.name]
|
|
225
|
+
mod.base? || mod.expansion?
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Only show progress for MOD(s) that need API calls
|
|
229
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Checking for updates", output: $stderr)
|
|
230
|
+
presenter.start(total: regular_mods.size)
|
|
231
|
+
|
|
232
|
+
pool = Concurrent::FixedThreadPool.new(DEFAULT_JOBS)
|
|
233
|
+
|
|
234
|
+
futures = regular_mods.map {|info|
|
|
235
|
+
Concurrent::Future.execute(executor: pool) do
|
|
236
|
+
result = fetch_latest_version_for_mod(info)
|
|
237
|
+
presenter.update
|
|
238
|
+
result
|
|
239
|
+
end
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
results = futures.map(&:value!)
|
|
243
|
+
presenter.finish
|
|
244
|
+
|
|
245
|
+
# Combine base/expansion (unchanged) with fetched results
|
|
246
|
+
base_and_expansions + results
|
|
247
|
+
ensure
|
|
248
|
+
pool&.shutdown
|
|
249
|
+
pool&.wait_for_termination
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Fetch latest version for a single MOD
|
|
253
|
+
#
|
|
254
|
+
# @param info [MODInfo] MOD info
|
|
255
|
+
# @return [MODInfo] MOD info with latest version
|
|
256
|
+
private def fetch_latest_version_for_mod(info)
|
|
257
|
+
portal_info = mod_portal_api.get_mod(info.name)
|
|
258
|
+
latest = portal_info[:releases]&.map {|r| MODVersion.from_string(r[:version]) }&.max
|
|
259
|
+
MODInfo.new(
|
|
260
|
+
name: info.name,
|
|
261
|
+
version: info.version,
|
|
262
|
+
enabled: info.enabled,
|
|
263
|
+
error: info.error,
|
|
264
|
+
latest_version: latest
|
|
265
|
+
)
|
|
266
|
+
rescue MODNotOnPortalError
|
|
267
|
+
logger.debug("MOD not found on portal", mod: info.name)
|
|
268
|
+
info
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Sort MODs: base -> expansion (alphabetically) -> others (alphabetically)
|
|
272
|
+
#
|
|
273
|
+
# @param mod_infos [Array<MODInfo>] MOD info list
|
|
274
|
+
# @return [Array<MODInfo>] sorted MOD info list
|
|
275
|
+
private def sort_mods(mod_infos)
|
|
276
|
+
mod_infos.sort_by do |info|
|
|
277
|
+
mod = Factorix::MOD[name: info.name]
|
|
278
|
+
if mod.base?
|
|
279
|
+
[0, info.name]
|
|
280
|
+
elsif mod.expansion?
|
|
281
|
+
[1, info.name]
|
|
282
|
+
else
|
|
283
|
+
[2, info.name]
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Output MOD list in table format
|
|
289
|
+
#
|
|
290
|
+
# @param mod_infos [Array<MODInfo>] MOD info list
|
|
291
|
+
# @param show_latest [Boolean] show LATEST column for outdated MODs
|
|
292
|
+
# @param active_filter [Symbol, nil] active filter (:enabled, :disabled, :errors, :outdated, or nil)
|
|
293
|
+
# @param total_count [Integer] total MOD count before filtering
|
|
294
|
+
# @return [void]
|
|
295
|
+
private def output_table(mod_infos, show_latest: false, active_filter: nil, total_count: 0)
|
|
296
|
+
if mod_infos.empty?
|
|
297
|
+
message = active_filter ? "No MOD(s) match the specified criteria" : "No MOD(s) found"
|
|
298
|
+
say message, prefix: :info
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Calculate column widths
|
|
303
|
+
name_width = [mod_infos.map {|m| m.name.length }.max, 4].max
|
|
304
|
+
version_width = [mod_infos.map {|m| m.version.to_s.length }.max, 7].max
|
|
305
|
+
|
|
306
|
+
if show_latest
|
|
307
|
+
latest_width = [mod_infos.map {|m| m.latest_version&.to_s&.length || 0 }.max, 6].max
|
|
308
|
+
|
|
309
|
+
# Header with LATEST column
|
|
310
|
+
puts "%-#{name_width}s %-#{version_width}s %-#{latest_width}s %s" % %w[NAME VERSION LATEST STATUS]
|
|
311
|
+
|
|
312
|
+
# Rows with LATEST column
|
|
313
|
+
mod_infos.each do |info|
|
|
314
|
+
puts "%-#{name_width}s %-#{version_width}s %-#{latest_width}s %s" % [info.name, info.version, info.latest_version, info.status]
|
|
315
|
+
end
|
|
316
|
+
else
|
|
317
|
+
# Header
|
|
318
|
+
puts "%-#{name_width}s %-#{version_width}s %s" % %w[NAME VERSION STATUS]
|
|
319
|
+
|
|
320
|
+
# Rows
|
|
321
|
+
mod_infos.each do |info|
|
|
322
|
+
puts "%-#{name_width}s %-#{version_width}s %s" % [info.name, info.version, info.status]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
say format_summary(mod_infos.size, active_filter, total_count), prefix: :info
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Format summary message based on active filter
|
|
330
|
+
#
|
|
331
|
+
# @param count [Integer] filtered MOD count
|
|
332
|
+
# @param active_filter [Symbol, nil] active filter
|
|
333
|
+
# @param total_count [Integer] total MOD count
|
|
334
|
+
# @return [String] formatted summary message
|
|
335
|
+
private def format_summary(count, active_filter, total_count)
|
|
336
|
+
case active_filter
|
|
337
|
+
when :enabled
|
|
338
|
+
"Summary: #{count} enabled MOD(s), #{total_count} total MOD(s)"
|
|
339
|
+
when :disabled
|
|
340
|
+
"Summary: #{count} disabled MOD(s), #{total_count} total MOD(s)"
|
|
341
|
+
when :errors
|
|
342
|
+
"Summary: #{count} MOD(s) with errors, #{total_count} total MOD(s)"
|
|
343
|
+
when :outdated
|
|
344
|
+
"Summary: #{count} outdated MOD(s), #{total_count} total MOD(s)"
|
|
345
|
+
else
|
|
346
|
+
"Summary: #{count} MOD(s)"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Output MOD list in JSON format
|
|
351
|
+
#
|
|
352
|
+
# @param mod_infos [Array<MODInfo>] MOD info list
|
|
353
|
+
# @return [void]
|
|
354
|
+
private def output_json(mod_infos)
|
|
355
|
+
data = mod_infos.map {|info|
|
|
356
|
+
{
|
|
357
|
+
name: info.name,
|
|
358
|
+
version: info.version.to_s,
|
|
359
|
+
enabled: info.enabled,
|
|
360
|
+
error: info.error
|
|
361
|
+
}.tap do |h|
|
|
362
|
+
h[:latest_version] = info.latest_version.to_s if info.latest_version
|
|
363
|
+
end
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
puts JSON.pretty_generate(data)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
# Search MODs on Factorio MOD Portal
|
|
10
|
+
class Search < Base
|
|
11
|
+
# @!parse
|
|
12
|
+
# # @return [Portal]
|
|
13
|
+
# attr_reader :portal
|
|
14
|
+
# # @return [Runtime]
|
|
15
|
+
# attr_reader :runtime
|
|
16
|
+
include Import[:portal, :runtime]
|
|
17
|
+
|
|
18
|
+
desc "Search MOD(s) on Factorio MOD Portal"
|
|
19
|
+
|
|
20
|
+
example [
|
|
21
|
+
" # List MOD(s) for current Factorio version",
|
|
22
|
+
"mod-a mod-b # Search specific MOD(s) by name",
|
|
23
|
+
"--sort name # List MOD(s) sorted by name",
|
|
24
|
+
"--page 2 --page-size 25 # Paginate results",
|
|
25
|
+
"--no-hide-deprecated # Include deprecated MOD(s)",
|
|
26
|
+
"--version 1.1 # Filter by specific Factorio version"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
argument :mod_names, type: :array, required: false, default: [], desc: "MOD names to search"
|
|
30
|
+
|
|
31
|
+
option :hide_deprecated, type: :boolean, default: true, desc: "Hide deprecated MOD(s)"
|
|
32
|
+
option :page, type: :integer, default: 1, desc: "Page number"
|
|
33
|
+
option :page_size, type: :integer, default: 25, desc: "Results per page (max 500)"
|
|
34
|
+
option :sort, type: :string, values: %w[name created_at updated_at], desc: "Sort field"
|
|
35
|
+
option :sort_order, type: :string, values: %w[asc desc], desc: "Sort order"
|
|
36
|
+
option :version, type: :string, desc: "Filter by Factorio version (default: installed version)"
|
|
37
|
+
option :json, type: :flag, default: false, desc: "Output in JSON format"
|
|
38
|
+
|
|
39
|
+
# Execute the search command
|
|
40
|
+
#
|
|
41
|
+
# @param mod_names [Array<String>] MOD names to search
|
|
42
|
+
# @param hide_deprecated [Boolean] Hide deprecated MODs
|
|
43
|
+
# @param page [Integer] Page number
|
|
44
|
+
# @param page_size [Integer] Results per page
|
|
45
|
+
# @param sort [String, nil] Sort field
|
|
46
|
+
# @param sort_order [String, nil] Sort order
|
|
47
|
+
# @param version [String, nil] Factorio version filter
|
|
48
|
+
# @param json [Boolean] Output in JSON format
|
|
49
|
+
# @return [void]
|
|
50
|
+
def call(mod_names: [], hide_deprecated: true, page: 1, page_size: 25, sort: nil, sort_order: nil, version: nil, json: false, **)
|
|
51
|
+
version ||= default_factorio_version
|
|
52
|
+
|
|
53
|
+
mods = portal.list_mods(*mod_names, hide_deprecated: hide_deprecated || nil, page:, page_size:, sort:, sort_order:, version:)
|
|
54
|
+
|
|
55
|
+
if json
|
|
56
|
+
output_json(mods)
|
|
57
|
+
else
|
|
58
|
+
output_table(mods)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def output_json(mods)
|
|
63
|
+
puts JSON.pretty_generate(mods.map {|mod| mod_to_hash(mod) })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def mod_to_hash(mod)
|
|
67
|
+
{
|
|
68
|
+
name: mod.name,
|
|
69
|
+
title: mod.title,
|
|
70
|
+
owner: mod.owner,
|
|
71
|
+
summary: mod.summary,
|
|
72
|
+
downloads_count: mod.downloads_count,
|
|
73
|
+
category: mod.category.value,
|
|
74
|
+
score: mod.score,
|
|
75
|
+
thumbnail: mod.thumbnail&.to_s,
|
|
76
|
+
latest_release: mod.latest_release && release_to_hash(mod.latest_release),
|
|
77
|
+
releases: mod.releases.map {|r| release_to_hash(r) }
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private def release_to_hash(release)
|
|
82
|
+
{
|
|
83
|
+
version: release.version.to_s,
|
|
84
|
+
file_name: release.file_name,
|
|
85
|
+
released_at: release.released_at.iso8601,
|
|
86
|
+
factorio_version: release.info_json[:factorio_version],
|
|
87
|
+
sha1: release.sha1
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private def output_table(mods)
|
|
92
|
+
if mods.empty?
|
|
93
|
+
say "No MOD(s) found", prefix: :info
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
rows = mods.map {|mod| format_row(mod) }
|
|
98
|
+
|
|
99
|
+
headers = %w[NAME TITLE CATEGORY OWNER LATEST]
|
|
100
|
+
widths = headers.map.with_index {|h, i| [h.length, *rows.map {|r| r[i].to_s.length }].max }
|
|
101
|
+
|
|
102
|
+
puts format_table_row(headers, widths)
|
|
103
|
+
|
|
104
|
+
rows.each do |row|
|
|
105
|
+
puts format_table_row(row, widths)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
say "#{mods.size} MOD(s) found", prefix: :info
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private def format_table_row(values, widths)
|
|
112
|
+
pairs = values.zip(widths)
|
|
113
|
+
pairs.map {|v, w| v.to_s.ljust(w) }.join(" ")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private def format_row(mod)
|
|
117
|
+
[
|
|
118
|
+
mod.name,
|
|
119
|
+
mod.title,
|
|
120
|
+
mod.category.name,
|
|
121
|
+
mod.owner,
|
|
122
|
+
mod.latest_release&.version&.to_s
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private def default_factorio_version
|
|
127
|
+
base_mod = InstalledMOD.from_directory(runtime.data_dir + "base")
|
|
128
|
+
"#{base_mod.version.major}.#{base_mod.version.minor}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
module Settings
|
|
10
|
+
# Dump MOD settings to JSON format
|
|
11
|
+
class Dump < Base
|
|
12
|
+
# @!parse
|
|
13
|
+
# # @return [Runtime::Base]
|
|
14
|
+
# attr_reader :runtime
|
|
15
|
+
include Import[:runtime]
|
|
16
|
+
|
|
17
|
+
desc "Dump MOD settings to JSON format"
|
|
18
|
+
|
|
19
|
+
example [
|
|
20
|
+
" # Dump to stdout",
|
|
21
|
+
"-o settings.json # Dump to file",
|
|
22
|
+
"/path/to/mod-settings.dat -o out.json # Dump specific file"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
argument :settings_file, type: :string, required: false, desc: "Path to mod-settings.dat file"
|
|
26
|
+
option :output, type: :string, aliases: ["-o"], desc: "Output file path"
|
|
27
|
+
|
|
28
|
+
# Execute the dump command
|
|
29
|
+
#
|
|
30
|
+
# @param settings_file [String, nil] Path to mod-settings.dat file
|
|
31
|
+
# @param output [String, nil] Output file path
|
|
32
|
+
# @return [void]
|
|
33
|
+
def call(settings_file: nil, output: nil, **)
|
|
34
|
+
# Load MOD settings
|
|
35
|
+
settings_path = settings_file ? Pathname(settings_file) : runtime.mod_settings_path
|
|
36
|
+
settings = MODSettings.load(settings_path)
|
|
37
|
+
|
|
38
|
+
# Convert to JSON format
|
|
39
|
+
data = build_hash(settings)
|
|
40
|
+
output_string = JSON.pretty_generate(data)
|
|
41
|
+
|
|
42
|
+
# Write to output
|
|
43
|
+
if output
|
|
44
|
+
Pathname(output).write(output_string)
|
|
45
|
+
else
|
|
46
|
+
puts output_string
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Build hash from MODSettings for JSON output
|
|
51
|
+
#
|
|
52
|
+
# @param settings [Factorix::MODSettings] The MOD settings to convert
|
|
53
|
+
# @return [Hash] Hash representation of the settings
|
|
54
|
+
private def build_hash(settings)
|
|
55
|
+
result = {
|
|
56
|
+
"game_version" => settings.game_version.to_s
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
settings.each_section do |section|
|
|
60
|
+
section_hash = {}
|
|
61
|
+
section.each do |key, value|
|
|
62
|
+
section_hash[key] = convert_value_for_output(value)
|
|
63
|
+
end
|
|
64
|
+
result[section.name] = section_hash unless section_hash.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Convert value for JSON output (handle SignedInteger/UnsignedInteger)
|
|
71
|
+
#
|
|
72
|
+
# @param value [Object] The value to convert
|
|
73
|
+
# @return [Object] Converted value
|
|
74
|
+
private def convert_value_for_output(value)
|
|
75
|
+
case value
|
|
76
|
+
when SerDes::SignedInteger, SerDes::UnsignedInteger
|
|
77
|
+
# Integer(...) does not accept Integer instance
|
|
78
|
+
Integer(value.to_s, 10)
|
|
79
|
+
else
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
module Settings
|
|
10
|
+
# Restore MOD settings from JSON format
|
|
11
|
+
class Restore < Base
|
|
12
|
+
require_game_stopped!
|
|
13
|
+
backup_support!
|
|
14
|
+
|
|
15
|
+
# @!parse
|
|
16
|
+
# # @return [Runtime::Base]
|
|
17
|
+
# attr_reader :runtime
|
|
18
|
+
include Import[:runtime]
|
|
19
|
+
|
|
20
|
+
desc "Restore MOD settings from JSON format"
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
"-i settings.json # Restore from file",
|
|
24
|
+
" # Restore from stdin"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
argument :settings_file, type: :string, required: false, desc: "Path to mod-settings.dat file to write"
|
|
28
|
+
option :input, type: :string, aliases: ["-i"], desc: "Input file path"
|
|
29
|
+
|
|
30
|
+
# Execute the restore command
|
|
31
|
+
#
|
|
32
|
+
# @param input [String, nil] Path to JSON file
|
|
33
|
+
# @param settings_file [String, nil] Path to mod-settings.dat file
|
|
34
|
+
# @return [void]
|
|
35
|
+
def call(input: nil, settings_file: nil, **)
|
|
36
|
+
# Read input
|
|
37
|
+
if input
|
|
38
|
+
input_path = Pathname(input)
|
|
39
|
+
input_string = input_path.read
|
|
40
|
+
else
|
|
41
|
+
# Read from stdin
|
|
42
|
+
input_string = $stdin.read
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse input
|
|
46
|
+
data = JSON.parse(input_string)
|
|
47
|
+
settings = build_settings(data)
|
|
48
|
+
|
|
49
|
+
# Determine output path
|
|
50
|
+
output_path = settings_file ? Pathname(settings_file) : runtime.mod_settings_path
|
|
51
|
+
|
|
52
|
+
# Backup existing file if it exists
|
|
53
|
+
backup_if_exists(output_path)
|
|
54
|
+
|
|
55
|
+
# Save settings
|
|
56
|
+
settings.save(output_path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Build MODSettings from parsed JSON data
|
|
60
|
+
#
|
|
61
|
+
# @param data [Hash] Parsed JSON data
|
|
62
|
+
# @return [Factorix::MODSettings] The MOD settings
|
|
63
|
+
private def build_settings(data)
|
|
64
|
+
game_version = GameVersion.from_string(data["game_version"])
|
|
65
|
+
sections = {}
|
|
66
|
+
|
|
67
|
+
MODSettings::VALID_SECTIONS.each do |section_name|
|
|
68
|
+
section = MODSettings::Section.new(section_name)
|
|
69
|
+
if data.key?(section_name)
|
|
70
|
+
data[section_name].each do |key, value|
|
|
71
|
+
section[key] = convert_value_for_input(value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
sections[section_name] = section
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
MODSettings.new(game_version, sections)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Convert value from JSON input (detect integer types)
|
|
81
|
+
#
|
|
82
|
+
# @param value [Object] The value to convert
|
|
83
|
+
# @return [Object] Converted value
|
|
84
|
+
# @note Factorio MOD settings use signed integers for int-setting type.
|
|
85
|
+
# Since JSON doesn't preserve signed/unsigned distinction,
|
|
86
|
+
# we use SignedInteger for all integer values.
|
|
87
|
+
# @see https://wiki.factorio.com/Tutorial:Mod_settings#int-setting
|
|
88
|
+
private def convert_value_for_input(value)
|
|
89
|
+
case value
|
|
90
|
+
when Integer
|
|
91
|
+
SerDes::SignedInteger.new(value)
|
|
92
|
+
else
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|