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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +1 -1
  4. data/exe/factorix +17 -0
  5. data/lib/factorix/api/mod_download_api.rb +11 -6
  6. data/lib/factorix/api/mod_info.rb +2 -2
  7. data/lib/factorix/api/mod_management_api.rb +1 -1
  8. data/lib/factorix/api/mod_portal_api.rb +6 -49
  9. data/lib/factorix/api_credential.rb +1 -1
  10. data/lib/factorix/cache/base.rb +116 -0
  11. data/lib/factorix/cache/entry.rb +25 -0
  12. data/lib/factorix/cache/file_system.rb +137 -57
  13. data/lib/factorix/cache/redis.rb +287 -0
  14. data/lib/factorix/cache/s3.rb +388 -0
  15. data/lib/factorix/cli/commands/backup_support.rb +1 -1
  16. data/lib/factorix/cli/commands/base.rb +3 -3
  17. data/lib/factorix/cli/commands/cache/evict.rb +19 -24
  18. data/lib/factorix/cli/commands/cache/stat.rb +66 -67
  19. data/lib/factorix/cli/commands/command_wrapper.rb +5 -5
  20. data/lib/factorix/cli/commands/completion.rb +1 -2
  21. data/lib/factorix/cli/commands/confirmable.rb +1 -1
  22. data/lib/factorix/cli/commands/download_support.rb +2 -7
  23. data/lib/factorix/cli/commands/mod/check.rb +1 -1
  24. data/lib/factorix/cli/commands/mod/disable.rb +1 -1
  25. data/lib/factorix/cli/commands/mod/download.rb +7 -7
  26. data/lib/factorix/cli/commands/mod/edit.rb +10 -13
  27. data/lib/factorix/cli/commands/mod/enable.rb +1 -1
  28. data/lib/factorix/cli/commands/mod/image/add.rb +3 -6
  29. data/lib/factorix/cli/commands/mod/image/edit.rb +2 -5
  30. data/lib/factorix/cli/commands/mod/image/list.rb +5 -8
  31. data/lib/factorix/cli/commands/mod/install.rb +7 -7
  32. data/lib/factorix/cli/commands/mod/list.rb +7 -7
  33. data/lib/factorix/cli/commands/mod/search.rb +13 -12
  34. data/lib/factorix/cli/commands/mod/settings/dump.rb +3 -3
  35. data/lib/factorix/cli/commands/mod/settings/restore.rb +2 -2
  36. data/lib/factorix/cli/commands/mod/show.rb +22 -23
  37. data/lib/factorix/cli/commands/mod/sync.rb +8 -8
  38. data/lib/factorix/cli/commands/mod/uninstall.rb +1 -1
  39. data/lib/factorix/cli/commands/mod/update.rb +11 -43
  40. data/lib/factorix/cli/commands/mod/upload.rb +7 -10
  41. data/lib/factorix/cli/commands/path.rb +2 -2
  42. data/lib/factorix/cli/commands/portal_support.rb +27 -0
  43. data/lib/factorix/cli/commands/version.rb +1 -1
  44. data/lib/factorix/container.rb +155 -0
  45. data/lib/factorix/dependency/parser.rb +1 -1
  46. data/lib/factorix/errors.rb +3 -0
  47. data/lib/factorix/http/cache_decorator.rb +5 -5
  48. data/lib/factorix/http/client.rb +3 -3
  49. data/lib/factorix/info_json.rb +7 -7
  50. data/lib/factorix/mod_list.rb +2 -2
  51. data/lib/factorix/mod_settings.rb +2 -2
  52. data/lib/factorix/portal.rb +3 -2
  53. data/lib/factorix/runtime/user_configurable.rb +9 -9
  54. data/lib/factorix/service_credential.rb +3 -3
  55. data/lib/factorix/transfer/downloader.rb +19 -11
  56. data/lib/factorix/version.rb +1 -1
  57. data/lib/factorix.rb +110 -1
  58. data/sig/factorix/api/mod_download_api.rbs +1 -2
  59. data/sig/factorix/cache/base.rbs +28 -0
  60. data/sig/factorix/cache/entry.rbs +14 -0
  61. data/sig/factorix/cache/file_system.rbs +7 -6
  62. data/sig/factorix/cache/redis.rbs +36 -0
  63. data/sig/factorix/cache/s3.rbs +38 -0
  64. data/sig/factorix/container.rbs +15 -0
  65. data/sig/factorix/errors.rbs +3 -0
  66. data/sig/factorix/portal.rbs +1 -1
  67. data/sig/factorix.rbs +99 -0
  68. metadata +27 -4
  69. data/lib/factorix/application.rb +0 -218
  70. 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
- @now = Time.now
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
- config = Application.config.cache.public_send(name)
57
- cache_dir = config.dir
59
+ cache = Container.resolve(:"#{name}_cache")
60
+ config = Factorix.config.cache.public_send(name)
58
61
 
59
- entries = scan_entries(cache_dir, config.ttl)
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
- stale_locks: count_stale_locks(cache_dir)
69
+ backend_info: cache.backend_info
70
70
  }
71
71
  end
72
72
 
73
- # Scan cache directory and collect entry information
73
+ # Collect cache entries using the cache interface
74
74
  #
75
- # @param cache_dir [Pathname] cache directory path
76
- # @param ttl [Integer, nil] time-to-live in seconds
77
- # @return [Array<Hash>] array of entry info hashes
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
- cache_dir.glob("**/*").each do |path|
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<Hash>] entry info 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[:expired] }
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<Hash>] entry info 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 {|e| e[:size] }
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<Hash>] entry info 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 {|e| e[:age] }
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 " Directory: #{data[:directory]}"
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
- puts " Stale locks: #{data[:stale_locks]}"
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 = Application[:logger]
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 = Application[:logger]
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
- Application.load_config(path)
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 = Application[:runtime].factorix_config_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 = Application[: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
- thread_downloader.subscribe(handler)
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: $stderr)
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: $stderr)
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[:portal, :logger, :runtime]
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, type: :string, aliases: ["-d"], default: ".", desc: "Download directory"
33
- option :jobs, type: :integer, aliases: ["-j"], default: 4, desc: "Number of parallel downloads"
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: $stderr)
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
- # @!parse
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, type: :string, required: true, desc: "MOD name"
23
- option :description, type: :string, desc: "Markdown description"
24
- option :summary, type: :string, desc: "Brief description"
25
- option :title, type: :string, desc: "MOD title"
26
- option :category, type: :string, desc: "MOD 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, type: :string, desc: "License identifier"
29
- option :homepage, type: :string, desc: "Homepage URL"
30
- option :source_url, type: :string, desc: "Repository URL"
31
- option :faq, type: :string, desc: "FAQ text"
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: $stderr)
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
- # @!parse
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, type: :string, required: true, desc: "MOD name"
22
- argument :image_file, type: :string, required: true, desc: "Path to 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
- # @!parse
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, type: :string, required: true, desc: "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
- # @!parse
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, type: :string, required: true, desc: "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[:portal, :logger, :runtime]
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, type: :integer, aliases: ["-j"], default: 4, desc: "Number of parallel downloads"
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: $stderr)
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: $stderr)
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: $stderr)
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: $stderr)
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