factorix 0.5.1 → 0.7.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 +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +1 -1
- data/exe/factorix +17 -0
- data/lib/factorix/api/mod_download_api.rb +11 -6
- data/lib/factorix/api/mod_info.rb +2 -2
- data/lib/factorix/api/mod_management_api.rb +1 -1
- data/lib/factorix/api/mod_portal_api.rb +6 -49
- data/lib/factorix/api_credential.rb +1 -1
- data/lib/factorix/cache/base.rb +116 -0
- data/lib/factorix/cache/entry.rb +25 -0
- data/lib/factorix/cache/file_system.rb +137 -57
- data/lib/factorix/cache/redis.rb +287 -0
- data/lib/factorix/cache/s3.rb +388 -0
- data/lib/factorix/cli/commands/backup_support.rb +1 -1
- data/lib/factorix/cli/commands/base.rb +3 -3
- data/lib/factorix/cli/commands/cache/evict.rb +19 -24
- data/lib/factorix/cli/commands/cache/stat.rb +66 -67
- data/lib/factorix/cli/commands/command_wrapper.rb +5 -5
- data/lib/factorix/cli/commands/completion.rb +1 -2
- data/lib/factorix/cli/commands/confirmable.rb +1 -1
- data/lib/factorix/cli/commands/download_support.rb +2 -7
- data/lib/factorix/cli/commands/mod/check.rb +1 -1
- data/lib/factorix/cli/commands/mod/disable.rb +1 -1
- data/lib/factorix/cli/commands/mod/download.rb +7 -7
- data/lib/factorix/cli/commands/mod/edit.rb +10 -13
- data/lib/factorix/cli/commands/mod/enable.rb +1 -1
- data/lib/factorix/cli/commands/mod/image/add.rb +3 -6
- data/lib/factorix/cli/commands/mod/image/edit.rb +2 -5
- data/lib/factorix/cli/commands/mod/image/list.rb +5 -8
- data/lib/factorix/cli/commands/mod/install.rb +7 -7
- data/lib/factorix/cli/commands/mod/list.rb +7 -7
- data/lib/factorix/cli/commands/mod/search.rb +13 -12
- data/lib/factorix/cli/commands/mod/settings/dump.rb +3 -3
- data/lib/factorix/cli/commands/mod/settings/restore.rb +2 -2
- data/lib/factorix/cli/commands/mod/show.rb +22 -23
- data/lib/factorix/cli/commands/mod/sync.rb +8 -8
- data/lib/factorix/cli/commands/mod/uninstall.rb +1 -1
- data/lib/factorix/cli/commands/mod/update.rb +11 -43
- data/lib/factorix/cli/commands/mod/upload.rb +7 -10
- data/lib/factorix/cli/commands/path.rb +2 -2
- data/lib/factorix/cli/commands/portal_support.rb +27 -0
- data/lib/factorix/cli/commands/version.rb +1 -1
- data/lib/factorix/container.rb +155 -0
- data/lib/factorix/dependency/parser.rb +1 -1
- data/lib/factorix/errors.rb +3 -0
- data/lib/factorix/http/cache_decorator.rb +5 -5
- data/lib/factorix/http/client.rb +3 -3
- data/lib/factorix/info_json.rb +7 -7
- data/lib/factorix/mod_list.rb +2 -2
- data/lib/factorix/mod_settings.rb +2 -2
- data/lib/factorix/portal.rb +3 -2
- data/lib/factorix/runtime/user_configurable.rb +9 -9
- data/lib/factorix/service_credential.rb +3 -3
- data/lib/factorix/transfer/downloader.rb +19 -11
- data/lib/factorix/version.rb +1 -1
- data/lib/factorix.rb +110 -1
- data/sig/factorix/api/mod_download_api.rbs +1 -2
- data/sig/factorix/cache/base.rbs +28 -0
- data/sig/factorix/cache/entry.rbs +14 -0
- data/sig/factorix/cache/file_system.rbs +7 -6
- data/sig/factorix/cache/redis.rbs +36 -0
- data/sig/factorix/cache/s3.rbs +38 -0
- data/sig/factorix/container.rbs +15 -0
- data/sig/factorix/errors.rbs +3 -0
- data/sig/factorix/portal.rbs +1 -1
- data/sig/factorix.rbs +99 -0
- metadata +27 -4
- data/lib/factorix/application.rb +0 -218
- data/sig/factorix/application.rbs +0 -86
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "dry/inflector"
|
|
3
4
|
require "json"
|
|
4
5
|
|
|
5
6
|
module Factorix
|
|
@@ -14,7 +15,6 @@ module Factorix
|
|
|
14
15
|
# @example
|
|
15
16
|
# $ factorix cache stat
|
|
16
17
|
# download:
|
|
17
|
-
# Directory: ~/.cache/factorix/download
|
|
18
18
|
# TTL: unlimited
|
|
19
19
|
# Entries: 42 / 42 (100.0% valid)
|
|
20
20
|
# ...
|
|
@@ -41,63 +41,52 @@ module Factorix
|
|
|
41
41
|
def call(json:, **)
|
|
42
42
|
logger.debug("Collecting cache statistics")
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
cache_names = Application.config.cache.values.keys
|
|
44
|
+
cache_names = Factorix.config.cache.values.keys
|
|
46
45
|
stats = cache_names.to_h {|name| [name, collect_stats(name)] }
|
|
47
46
|
|
|
48
47
|
if json
|
|
49
|
-
puts JSON.pretty_generate(stats)
|
|
48
|
+
out.puts JSON.pretty_generate(stats)
|
|
50
49
|
else
|
|
51
50
|
output_text(stats)
|
|
52
51
|
end
|
|
53
52
|
end
|
|
54
53
|
|
|
54
|
+
# Collect statistics for a cache
|
|
55
|
+
#
|
|
56
|
+
# @param name [Symbol] cache name
|
|
57
|
+
# @return [Hash] cache statistics
|
|
55
58
|
private def collect_stats(name)
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
cache = Container.resolve(:"#{name}_cache")
|
|
60
|
+
config = Factorix.config.cache.public_send(name)
|
|
58
61
|
|
|
59
|
-
entries = scan_entries(
|
|
62
|
+
entries = scan_entries(cache)
|
|
60
63
|
|
|
61
64
|
{
|
|
62
|
-
directory: cache_dir.to_s,
|
|
63
65
|
ttl: config.ttl,
|
|
64
|
-
max_file_size: config.max_file_size,
|
|
65
|
-
compression_threshold: config.compression_threshold,
|
|
66
66
|
entries: build_entry_stats(entries),
|
|
67
67
|
size: build_size_stats(entries),
|
|
68
68
|
age: build_age_stats(entries),
|
|
69
|
-
|
|
69
|
+
backend_info: cache.backend_info
|
|
70
70
|
}
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
#
|
|
73
|
+
# Collect cache entries using the cache interface
|
|
74
74
|
#
|
|
75
|
-
# @param
|
|
76
|
-
# @
|
|
77
|
-
|
|
78
|
-
private def scan_entries(cache_dir, ttl)
|
|
79
|
-
return [] unless cache_dir.exist?
|
|
80
|
-
|
|
75
|
+
# @param cache [Cache::Base] cache instance
|
|
76
|
+
# @return [Array<Cache::Entry>] array of cache entries
|
|
77
|
+
private def scan_entries(cache)
|
|
81
78
|
entries = []
|
|
82
|
-
|
|
83
|
-
next unless path.file?
|
|
84
|
-
next if path.extname == ".lock"
|
|
85
|
-
|
|
86
|
-
age_seconds = @now - path.mtime
|
|
87
|
-
expired = ttl ? age_seconds > ttl : false
|
|
88
|
-
|
|
89
|
-
entries << {size: path.size, age: age_seconds, expired:}
|
|
90
|
-
end
|
|
79
|
+
cache.each {|_key, entry| entries << entry }
|
|
91
80
|
entries
|
|
92
81
|
end
|
|
93
82
|
|
|
94
83
|
# Build entry count statistics
|
|
95
84
|
#
|
|
96
|
-
# @param entries [Array<
|
|
85
|
+
# @param entries [Array<Cache::Entry>] entry array
|
|
97
86
|
# @return [Hash] entry statistics
|
|
98
87
|
private def build_entry_stats(entries)
|
|
99
88
|
total = entries.size
|
|
100
|
-
valid = entries.count {|e| !e
|
|
89
|
+
valid = entries.count {|e| !e.expired? }
|
|
101
90
|
expired = total - valid
|
|
102
91
|
|
|
103
92
|
{total:, valid:, expired:}
|
|
@@ -105,45 +94,34 @@ module Factorix
|
|
|
105
94
|
|
|
106
95
|
# Build size statistics
|
|
107
96
|
#
|
|
108
|
-
# @param entries [Array<
|
|
97
|
+
# @param entries [Array<Cache::Entry>] entry array
|
|
109
98
|
# @return [Hash] size statistics
|
|
110
99
|
private def build_size_stats(entries)
|
|
111
100
|
return {total: 0, avg: 0, min: 0, max: 0} if entries.empty?
|
|
112
101
|
|
|
113
|
-
sizes = entries.map
|
|
102
|
+
sizes = entries.map(&:size)
|
|
114
103
|
{total: sizes.sum, avg: sizes.sum / sizes.size, min: sizes.min, max: sizes.max}
|
|
115
104
|
end
|
|
116
105
|
|
|
117
106
|
# Build age statistics
|
|
118
107
|
#
|
|
119
|
-
# @param entries [Array<
|
|
108
|
+
# @param entries [Array<Cache::Entry>] entry array
|
|
120
109
|
# @return [Hash] age statistics
|
|
121
110
|
private def build_age_stats(entries)
|
|
122
111
|
return {oldest: nil, newest: nil, avg: nil} if entries.empty?
|
|
123
112
|
|
|
124
|
-
ages = entries.map
|
|
113
|
+
ages = entries.map(&:age)
|
|
125
114
|
{oldest: ages.max, newest: ages.min, avg: ages.sum / ages.size}
|
|
126
115
|
end
|
|
127
116
|
|
|
128
|
-
# Count stale lock files
|
|
129
|
-
#
|
|
130
|
-
# @param cache_dir [Pathname] cache directory path
|
|
131
|
-
# @return [Integer] number of stale lock files
|
|
132
|
-
private def count_stale_locks(cache_dir)
|
|
133
|
-
return 0 unless cache_dir.exist?
|
|
134
|
-
|
|
135
|
-
lock_lifetime = Factorix::Cache::FileSystem::LOCK_FILE_LIFETIME
|
|
136
|
-
cache_dir.glob("**/*.lock").count {|path| @now - path.mtime > lock_lifetime }
|
|
137
|
-
end
|
|
138
|
-
|
|
139
117
|
# Output statistics in text format (ccache-style)
|
|
140
118
|
#
|
|
141
119
|
# @param stats [Hash] statistics for all caches
|
|
142
120
|
# @return [void]
|
|
143
121
|
private def output_text(stats)
|
|
144
122
|
stats.each_with_index do |(name, data), index|
|
|
145
|
-
puts if index > 0
|
|
146
|
-
puts "#{name}:"
|
|
123
|
+
out.puts if index > 0
|
|
124
|
+
out.puts "#{name}:"
|
|
147
125
|
output_cache_stats(data)
|
|
148
126
|
end
|
|
149
127
|
end
|
|
@@ -153,26 +131,59 @@ module Factorix
|
|
|
153
131
|
# @param data [Hash] cache statistics
|
|
154
132
|
# @return [void]
|
|
155
133
|
private def output_cache_stats(data)
|
|
156
|
-
puts "
|
|
157
|
-
puts " TTL: #{format_ttl(data[:ttl])}"
|
|
158
|
-
puts " Max file size: #{format_size(data[:max_file_size])}"
|
|
159
|
-
puts " Compression: #{format_compression(data[:compression_threshold])}"
|
|
134
|
+
out.puts " TTL: #{format_ttl(data[:ttl])}"
|
|
160
135
|
|
|
161
136
|
entries = data[:entries]
|
|
162
137
|
valid_pct = entries[:total] > 0 ? (Float(entries[:valid]) / entries[:total] * 100) : 0.0
|
|
163
|
-
puts " Entries: #{entries[:valid]} / #{entries[:total]} (#{"%.1f" % valid_pct}% valid)"
|
|
138
|
+
out.puts " Entries: #{entries[:valid]} / #{entries[:total]} (#{"%.1f" % valid_pct}% valid)"
|
|
164
139
|
|
|
165
140
|
size = data[:size]
|
|
166
|
-
puts " Size: #{format_size(size[:total])} (avg #{format_size(size[:avg])})"
|
|
141
|
+
out.puts " Size: #{format_size(size[:total])} (avg #{format_size(size[:avg])})"
|
|
167
142
|
|
|
168
143
|
age = data[:age]
|
|
169
144
|
if age[:oldest]
|
|
170
|
-
puts " Age: #{format_duration(age[:newest])} - #{format_duration(age[:oldest])} (avg #{format_duration(age[:avg])})"
|
|
145
|
+
out.puts " Age: #{format_duration(age[:newest])} - #{format_duration(age[:oldest])} (avg #{format_duration(age[:avg])})"
|
|
171
146
|
else
|
|
172
|
-
puts " Age: -"
|
|
147
|
+
out.puts " Age: -"
|
|
173
148
|
end
|
|
174
149
|
|
|
175
|
-
|
|
150
|
+
output_backend_info(data[:backend_info])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
INFLECTOR = Dry::Inflector.new do |inflections|
|
|
154
|
+
inflections.acronym("URL")
|
|
155
|
+
end
|
|
156
|
+
private_constant :INFLECTOR
|
|
157
|
+
|
|
158
|
+
# Output backend-specific information
|
|
159
|
+
#
|
|
160
|
+
# @param info [Hash] backend-specific information
|
|
161
|
+
# @return [void]
|
|
162
|
+
private def output_backend_info(info)
|
|
163
|
+
return if info.empty?
|
|
164
|
+
|
|
165
|
+
out.puts " Backend:"
|
|
166
|
+
info.each do |key, value|
|
|
167
|
+
label = INFLECTOR.humanize(key)
|
|
168
|
+
formatted_value = format_backend_value(key, value)
|
|
169
|
+
out.puts " %-20s %s" % [label + ":", formatted_value]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Format a backend info value for display
|
|
174
|
+
#
|
|
175
|
+
# @param key [Symbol] the key name
|
|
176
|
+
# @param value [Object] the value to format
|
|
177
|
+
# @return [String] formatted value
|
|
178
|
+
private def format_backend_value(key, value)
|
|
179
|
+
case key
|
|
180
|
+
when :max_file_size, :compression_threshold
|
|
181
|
+
value.nil? ? "unlimited" : format_size(value)
|
|
182
|
+
when :lock_timeout
|
|
183
|
+
format_duration(value)
|
|
184
|
+
else
|
|
185
|
+
value.to_s
|
|
186
|
+
end
|
|
176
187
|
end
|
|
177
188
|
|
|
178
189
|
# Format TTL value for display
|
|
@@ -182,18 +193,6 @@ module Factorix
|
|
|
182
193
|
private def format_ttl(ttl)
|
|
183
194
|
ttl.nil? ? "unlimited" : format_duration(ttl)
|
|
184
195
|
end
|
|
185
|
-
|
|
186
|
-
# Format compression threshold for display
|
|
187
|
-
#
|
|
188
|
-
# @param threshold [Integer, nil] compression threshold in bytes
|
|
189
|
-
# @return [String] formatted compression setting
|
|
190
|
-
private def format_compression(threshold)
|
|
191
|
-
case threshold
|
|
192
|
-
when nil then "disabled"
|
|
193
|
-
when 0 then "enabled (always)"
|
|
194
|
-
else "enabled (>= #{format_size(threshold)})"
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
196
|
end
|
|
198
197
|
end
|
|
199
198
|
end
|
|
@@ -23,14 +23,14 @@ module Factorix
|
|
|
23
23
|
super
|
|
24
24
|
rescue Error => e
|
|
25
25
|
# Expected errors (validation failures, missing dependencies, etc.)
|
|
26
|
-
log =
|
|
26
|
+
log = Container[:logger]
|
|
27
27
|
log.warn(e.message)
|
|
28
28
|
log.debug(e)
|
|
29
29
|
say "Error: #{e.message}", prefix: :error unless @quiet
|
|
30
30
|
raise # Re-raise for exe/factorix to handle exit code
|
|
31
31
|
rescue => e
|
|
32
32
|
# Unexpected errors (bugs, system failures, etc.)
|
|
33
|
-
log =
|
|
33
|
+
log = Container[:logger]
|
|
34
34
|
log.error(e)
|
|
35
35
|
say "Unexpected error: #{e.message}", prefix: :error unless @quiet
|
|
36
36
|
raise # Re-raise for exe/factorix to handle exit code
|
|
@@ -40,7 +40,7 @@ module Factorix
|
|
|
40
40
|
path = resolve_config_path(explicit_path)
|
|
41
41
|
return unless path
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Factorix.load_config(path)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Resolves which config path to use
|
|
@@ -50,14 +50,14 @@ module Factorix
|
|
|
50
50
|
return Pathname(explicit_path) if explicit_path
|
|
51
51
|
return Pathname(ENV.fetch("FACTORIX_CONFIG")) if ENV["FACTORIX_CONFIG"]
|
|
52
52
|
|
|
53
|
-
default_path =
|
|
53
|
+
default_path = Container[:runtime].factorix_config_path
|
|
54
54
|
default_path.exist? ? default_path : nil
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Sets the application logger's level
|
|
58
58
|
# @param level [String] log level (debug, info, warn, error, fatal)
|
|
59
59
|
private def log_level!(level)
|
|
60
|
-
logger =
|
|
60
|
+
logger = Container[:logger]
|
|
61
61
|
level_constant = Logger.const_get(level.upcase)
|
|
62
62
|
|
|
63
63
|
# Change only the File backend (first backend) level
|
|
@@ -28,7 +28,6 @@ module Factorix
|
|
|
28
28
|
desc "Generate shell completion script"
|
|
29
29
|
|
|
30
30
|
argument :shell,
|
|
31
|
-
type: :string,
|
|
32
31
|
required: false,
|
|
33
32
|
values: [nil] + SUPPORTED_SHELLS.keys,
|
|
34
33
|
desc: "Shell type. Defaults to current shell from $SHELL"
|
|
@@ -53,7 +52,7 @@ module Factorix
|
|
|
53
52
|
script_path = COMPLETION_DIR / SUPPORTED_SHELLS[shell_type]
|
|
54
53
|
raise ConfigurationError, "#{shell_type.capitalize} completion script not found" unless script_path.exist?
|
|
55
54
|
|
|
56
|
-
puts script_path.read
|
|
55
|
+
out.puts script_path.read
|
|
57
56
|
end
|
|
58
57
|
|
|
59
58
|
# Detect shell type from SHELL environment variable
|
|
@@ -41,7 +41,7 @@ module Factorix
|
|
|
41
41
|
raise InvalidOperationError, "Cannot prompt for confirmation in quiet mode. Use --yes to proceed automatically."
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
print "#{message} [y/N] "
|
|
44
|
+
out.print "#{message} [y/N] "
|
|
45
45
|
response = $stdin.gets&.strip&.downcase
|
|
46
46
|
|
|
47
47
|
# Only explicit y or yes means yes (default is no for safety)
|
|
@@ -91,24 +91,19 @@ module Factorix
|
|
|
91
91
|
# @param jobs [Integer] Number of parallel downloads
|
|
92
92
|
# @return [void]
|
|
93
93
|
private def download_mods(targets, jobs)
|
|
94
|
-
multi_presenter = Progress::MultiPresenter.new(title: "\u{1F4E5}\u{FE0E} Downloads")
|
|
94
|
+
multi_presenter = Progress::MultiPresenter.new(title: "\u{1F4E5}\u{FE0E} Downloads", output: err)
|
|
95
95
|
|
|
96
96
|
pool = Concurrent::FixedThreadPool.new(jobs)
|
|
97
97
|
|
|
98
98
|
futures = targets.map {|target|
|
|
99
99
|
Concurrent::Future.execute(executor: pool) do
|
|
100
|
-
thread_portal = Application[:portal]
|
|
101
|
-
thread_downloader = thread_portal.mod_download_api.downloader
|
|
102
|
-
|
|
103
100
|
presenter = multi_presenter.register(
|
|
104
101
|
target[:mod].name,
|
|
105
102
|
title: target[:release].file_name
|
|
106
103
|
)
|
|
107
104
|
handler = Progress::DownloadHandler.new(presenter)
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
thread_portal.download_mod(target[:release], target[:output_path])
|
|
111
|
-
thread_downloader.unsubscribe(handler)
|
|
106
|
+
portal.download_mod(target[:release], target[:output_path], handler:)
|
|
112
107
|
end
|
|
113
108
|
}
|
|
114
109
|
|
|
@@ -24,7 +24,7 @@ module Factorix
|
|
|
24
24
|
# @return [void]
|
|
25
25
|
def call(**)
|
|
26
26
|
mod_list = MODList.load
|
|
27
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
27
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
28
28
|
handler = Progress::ScanHandler.new(presenter)
|
|
29
29
|
installed_mods = InstalledMOD.all(handler:)
|
|
30
30
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -39,7 +39,7 @@ module Factorix
|
|
|
39
39
|
|
|
40
40
|
# Without validation to allow fixing issues
|
|
41
41
|
mod_list = MODList.load
|
|
42
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
42
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
43
43
|
handler = Progress::ScanHandler.new(presenter)
|
|
44
44
|
installed_mods = InstalledMOD.all(handler:)
|
|
45
45
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -10,14 +10,13 @@ module Factorix
|
|
|
10
10
|
# Download MOD files from Factorio MOD Portal
|
|
11
11
|
class Download < Base
|
|
12
12
|
include DownloadSupport
|
|
13
|
+
include PortalSupport
|
|
13
14
|
# @!parse
|
|
14
|
-
# # @return [Portal]
|
|
15
|
-
# attr_reader :portal
|
|
16
15
|
# # @return [Dry::Logger::Dispatcher]
|
|
17
16
|
# attr_reader :logger
|
|
18
17
|
# # @return [Runtime]
|
|
19
18
|
# attr_reader :runtime
|
|
20
|
-
include Import[:
|
|
19
|
+
include Import[:logger, :runtime]
|
|
21
20
|
|
|
22
21
|
desc "Download MOD files from Factorio MOD Portal"
|
|
23
22
|
|
|
@@ -29,8 +28,8 @@ module Factorix
|
|
|
29
28
|
]
|
|
30
29
|
|
|
31
30
|
argument :mod_specs, type: :array, required: true, desc: "MOD specifications (name@version or name@latest or name)"
|
|
32
|
-
option :directory,
|
|
33
|
-
option :jobs,
|
|
31
|
+
option :directory, aliases: ["-d"], default: ".", desc: "Download directory"
|
|
32
|
+
option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
|
|
34
33
|
option :recursive, type: :flag, aliases: ["-r"], default: false, desc: "Include required dependencies recursively"
|
|
35
34
|
|
|
36
35
|
# Execute the download command
|
|
@@ -40,7 +39,8 @@ module Factorix
|
|
|
40
39
|
# @param jobs [Integer] Number of parallel downloads
|
|
41
40
|
# @param recursive [Boolean] Include required dependencies recursively
|
|
42
41
|
# @return [void]
|
|
43
|
-
def call(mod_specs:, directory: ".", jobs: 4, recursive: false, **)
|
|
42
|
+
def call(mod_specs:, directory: ".", jobs: "4", recursive: false, **)
|
|
43
|
+
jobs = Integer(jobs)
|
|
44
44
|
download_dir = Pathname(directory).expand_path
|
|
45
45
|
|
|
46
46
|
raise DirectoryNotFoundError, "Download directory does not exist: #{download_dir}" unless download_dir.exist?
|
|
@@ -69,7 +69,7 @@ module Factorix
|
|
|
69
69
|
# @param recursive [Boolean] Include dependencies
|
|
70
70
|
# @return [Array<Hash>] Download targets with MOD info and releases
|
|
71
71
|
private def plan_download(mod_specs, download_dir, jobs, recursive)
|
|
72
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output:
|
|
72
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
|
|
73
73
|
|
|
74
74
|
target_infos = fetch_target_mod_info(mod_specs, jobs, presenter)
|
|
75
75
|
|
|
@@ -6,10 +6,7 @@ module Factorix
|
|
|
6
6
|
module MOD
|
|
7
7
|
# Edit MOD metadata on Factorio MOD Portal
|
|
8
8
|
class Edit < Base
|
|
9
|
-
|
|
10
|
-
# # @return [Portal]
|
|
11
|
-
# attr_reader :portal
|
|
12
|
-
include Import[:portal]
|
|
9
|
+
include PortalSupport
|
|
13
10
|
|
|
14
11
|
desc "Edit MOD metadata on Factorio MOD Portal"
|
|
15
12
|
|
|
@@ -19,16 +16,16 @@ module Factorix
|
|
|
19
16
|
"some-mod --deprecated # Mark as deprecated"
|
|
20
17
|
]
|
|
21
18
|
|
|
22
|
-
argument :mod_name,
|
|
23
|
-
option :description,
|
|
24
|
-
option :summary,
|
|
25
|
-
option :title,
|
|
26
|
-
option :category,
|
|
19
|
+
argument :mod_name, required: true, desc: "MOD name"
|
|
20
|
+
option :description, desc: "Markdown description"
|
|
21
|
+
option :summary, desc: "Brief description"
|
|
22
|
+
option :title, desc: "MOD title"
|
|
23
|
+
option :category, desc: "MOD category"
|
|
27
24
|
option :tags, type: :array, desc: "Array of tags"
|
|
28
|
-
option :license,
|
|
29
|
-
option :homepage,
|
|
30
|
-
option :source_url,
|
|
31
|
-
option :faq,
|
|
25
|
+
option :license, desc: "License identifier"
|
|
26
|
+
option :homepage, desc: "Homepage URL"
|
|
27
|
+
option :source_url, desc: "Repository URL"
|
|
28
|
+
option :faq, desc: "FAQ text"
|
|
32
29
|
option :deprecated, type: :boolean, desc: "Deprecation flag"
|
|
33
30
|
|
|
34
31
|
# Execute the edit command
|
|
@@ -33,7 +33,7 @@ module Factorix
|
|
|
33
33
|
def call(mod_names:, **)
|
|
34
34
|
# Load current state (without validation to allow fixing issues)
|
|
35
35
|
mod_list = MODList.load
|
|
36
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
36
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
37
37
|
handler = Progress::ScanHandler.new(presenter)
|
|
38
38
|
installed_mods = InstalledMOD.all(handler:)
|
|
39
39
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -7,10 +7,7 @@ module Factorix
|
|
|
7
7
|
module Image
|
|
8
8
|
# Add an image to a MOD on Factorio MOD Portal
|
|
9
9
|
class Add < Base
|
|
10
|
-
|
|
11
|
-
# # @return [Portal]
|
|
12
|
-
# attr_reader :portal
|
|
13
|
-
include Import[:portal]
|
|
10
|
+
include PortalSupport
|
|
14
11
|
|
|
15
12
|
desc "Add an image to a MOD"
|
|
16
13
|
|
|
@@ -18,8 +15,8 @@ module Factorix
|
|
|
18
15
|
"some-mod screenshot.png # Add image to MOD"
|
|
19
16
|
]
|
|
20
17
|
|
|
21
|
-
argument :mod_name,
|
|
22
|
-
argument :image_file,
|
|
18
|
+
argument :mod_name, required: true, desc: "MOD name"
|
|
19
|
+
argument :image_file, required: true, desc: "Path to image file"
|
|
23
20
|
|
|
24
21
|
# Execute the add command
|
|
25
22
|
#
|
|
@@ -7,10 +7,7 @@ module Factorix
|
|
|
7
7
|
module Image
|
|
8
8
|
# Edit MOD's image list on Factorio MOD Portal
|
|
9
9
|
class Edit < Base
|
|
10
|
-
|
|
11
|
-
# # @return [Portal]
|
|
12
|
-
# attr_reader :portal
|
|
13
|
-
include Import[:portal]
|
|
10
|
+
include PortalSupport
|
|
14
11
|
|
|
15
12
|
desc "Edit MOD's image list (reorder/remove images)"
|
|
16
13
|
|
|
@@ -18,7 +15,7 @@ module Factorix
|
|
|
18
15
|
"some-mod abc123 def456 # Set image order (IDs from 'image list')"
|
|
19
16
|
]
|
|
20
17
|
|
|
21
|
-
argument :mod_name,
|
|
18
|
+
argument :mod_name, required: true, desc: "MOD name"
|
|
22
19
|
argument :image_ids, type: :array, required: true, desc: "Image IDs in desired order"
|
|
23
20
|
|
|
24
21
|
# Execute the edit command
|
|
@@ -7,10 +7,7 @@ module Factorix
|
|
|
7
7
|
module Image
|
|
8
8
|
# List images for a MOD on Factorio MOD Portal
|
|
9
9
|
class List < Base
|
|
10
|
-
|
|
11
|
-
# # @return [Portal]
|
|
12
|
-
# attr_reader :portal
|
|
13
|
-
include Import[:portal]
|
|
10
|
+
include PortalSupport
|
|
14
11
|
|
|
15
12
|
desc "List images for a MOD"
|
|
16
13
|
|
|
@@ -19,7 +16,7 @@ module Factorix
|
|
|
19
16
|
"some-mod --json # List images in JSON format"
|
|
20
17
|
]
|
|
21
18
|
|
|
22
|
-
argument :mod_name,
|
|
19
|
+
argument :mod_name, required: true, desc: "MOD name"
|
|
23
20
|
|
|
24
21
|
option :json, type: :flag, default: false, desc: "Output in JSON format"
|
|
25
22
|
|
|
@@ -45,7 +42,7 @@ module Factorix
|
|
|
45
42
|
end
|
|
46
43
|
|
|
47
44
|
if json
|
|
48
|
-
puts JSON.pretty_generate(images)
|
|
45
|
+
out.puts JSON.pretty_generate(images)
|
|
49
46
|
else
|
|
50
47
|
output_table(images)
|
|
51
48
|
end
|
|
@@ -60,10 +57,10 @@ module Factorix
|
|
|
60
57
|
id_width = [images.map {|i| i[:id].length }.max, 2].max
|
|
61
58
|
thumb_width = [images.map {|i| i[:thumbnail].length }.max, 9].max
|
|
62
59
|
|
|
63
|
-
puts "%-#{id_width}s %-#{thumb_width}s %s" % %w[ID THUMBNAIL URL]
|
|
60
|
+
out.puts "%-#{id_width}s %-#{thumb_width}s %s" % %w[ID THUMBNAIL URL]
|
|
64
61
|
|
|
65
62
|
images.each do |image|
|
|
66
|
-
puts "%-#{id_width}s %-#{thumb_width}s %s" % [image[:id], image[:thumbnail], image[:url]]
|
|
63
|
+
out.puts "%-#{id_width}s %-#{thumb_width}s %s" % [image[:id], image[:thumbnail], image[:url]]
|
|
67
64
|
end
|
|
68
65
|
end
|
|
69
66
|
end
|
|
@@ -14,15 +14,14 @@ module Factorix
|
|
|
14
14
|
backup_support!
|
|
15
15
|
|
|
16
16
|
include DownloadSupport
|
|
17
|
+
include PortalSupport
|
|
17
18
|
|
|
18
19
|
# @!parse
|
|
19
|
-
# # @return [Portal]
|
|
20
|
-
# attr_reader :portal
|
|
21
20
|
# # @return [Dry::Logger::Dispatcher]
|
|
22
21
|
# attr_reader :logger
|
|
23
22
|
# # @return [Factorix::Runtime]
|
|
24
23
|
# attr_reader :runtime
|
|
25
|
-
include Import[:
|
|
24
|
+
include Import[:logger, :runtime]
|
|
26
25
|
|
|
27
26
|
desc "Install MOD(s) from Factorio MOD Portal (downloads to MOD directory and enables)"
|
|
28
27
|
|
|
@@ -34,17 +33,18 @@ module Factorix
|
|
|
34
33
|
]
|
|
35
34
|
|
|
36
35
|
argument :mod_specs, type: :array, required: true, desc: "MOD specifications (name@version or name@latest or name)"
|
|
37
|
-
option :jobs,
|
|
36
|
+
option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
|
|
38
37
|
|
|
39
38
|
# Execute the install command
|
|
40
39
|
#
|
|
41
40
|
# @param mod_specs [Array<String>] MOD specifications
|
|
42
41
|
# @param jobs [Integer] Number of parallel downloads
|
|
43
42
|
# @return [void]
|
|
44
|
-
def call(mod_specs:, jobs: 4, **)
|
|
43
|
+
def call(mod_specs:, jobs: "4", **)
|
|
44
|
+
jobs = Integer(jobs)
|
|
45
45
|
# Load current state (without validation to allow fixing issues)
|
|
46
46
|
mod_list = MODList.load
|
|
47
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
47
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
48
48
|
handler = Progress::ScanHandler.new(presenter)
|
|
49
49
|
installed_mods = InstalledMOD.all(handler:)
|
|
50
50
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -128,7 +128,7 @@ module Factorix
|
|
|
128
128
|
# @return [Array<Hash>] Installation targets with MOD info and releases
|
|
129
129
|
private def plan_installation(mod_specs, graph, jobs)
|
|
130
130
|
# Create progress presenter for info fetching
|
|
131
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output:
|
|
131
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
|
|
132
132
|
|
|
133
133
|
# Phase 1: Fetch info for target MODs
|
|
134
134
|
target_infos = fetch_target_mod_info(mod_specs, jobs, presenter)
|
|
@@ -80,7 +80,7 @@ module Factorix
|
|
|
80
80
|
validate_filter_options!(enabled:, disabled:, errors:, outdated:)
|
|
81
81
|
|
|
82
82
|
mod_list = MODList.load
|
|
83
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
83
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
84
84
|
handler = Progress::ScanHandler.new(presenter)
|
|
85
85
|
installed_mods = InstalledMOD.all(handler:)
|
|
86
86
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -226,7 +226,7 @@ module Factorix
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
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:
|
|
229
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Checking for updates", output: err)
|
|
230
230
|
presenter.start(total: regular_mods.size)
|
|
231
231
|
|
|
232
232
|
pool = Concurrent::FixedThreadPool.new(DEFAULT_JOBS)
|
|
@@ -307,19 +307,19 @@ module Factorix
|
|
|
307
307
|
latest_width = [mod_infos.map {|m| m.latest_version&.to_s&.length || 0 }.max, 6].max
|
|
308
308
|
|
|
309
309
|
# Header with LATEST column
|
|
310
|
-
puts "%-#{name_width}s %-#{version_width}s %-#{latest_width}s %s" % %w[NAME VERSION LATEST STATUS]
|
|
310
|
+
out.puts "%-#{name_width}s %-#{version_width}s %-#{latest_width}s %s" % %w[NAME VERSION LATEST STATUS]
|
|
311
311
|
|
|
312
312
|
# Rows with LATEST column
|
|
313
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]
|
|
314
|
+
out.puts "%-#{name_width}s %-#{version_width}s %-#{latest_width}s %s" % [info.name, info.version, info.latest_version, info.status]
|
|
315
315
|
end
|
|
316
316
|
else
|
|
317
317
|
# Header
|
|
318
|
-
puts "%-#{name_width}s %-#{version_width}s %s" % %w[NAME VERSION STATUS]
|
|
318
|
+
out.puts "%-#{name_width}s %-#{version_width}s %s" % %w[NAME VERSION STATUS]
|
|
319
319
|
|
|
320
320
|
# Rows
|
|
321
321
|
mod_infos.each do |info|
|
|
322
|
-
puts "%-#{name_width}s %-#{version_width}s %s" % [info.name, info.version, info.status]
|
|
322
|
+
out.puts "%-#{name_width}s %-#{version_width}s %s" % [info.name, info.version, info.status]
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
@@ -363,7 +363,7 @@ module Factorix
|
|
|
363
363
|
end
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
puts JSON.pretty_generate(data)
|
|
366
|
+
out.puts JSON.pretty_generate(data)
|
|
367
367
|
end
|
|
368
368
|
end
|
|
369
369
|
end
|