factorix 0.5.1 → 0.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +1 -1
  4. data/lib/factorix/api/mod_download_api.rb +1 -1
  5. data/lib/factorix/api/mod_info.rb +2 -2
  6. data/lib/factorix/api/mod_management_api.rb +1 -1
  7. data/lib/factorix/api_credential.rb +1 -1
  8. data/lib/factorix/cli/commands/backup_support.rb +1 -1
  9. data/lib/factorix/cli/commands/base.rb +3 -3
  10. data/lib/factorix/cli/commands/cache/evict.rb +3 -3
  11. data/lib/factorix/cli/commands/cache/stat.rb +15 -15
  12. data/lib/factorix/cli/commands/command_wrapper.rb +5 -5
  13. data/lib/factorix/cli/commands/completion.rb +1 -2
  14. data/lib/factorix/cli/commands/confirmable.rb +1 -1
  15. data/lib/factorix/cli/commands/download_support.rb +2 -2
  16. data/lib/factorix/cli/commands/mod/check.rb +1 -1
  17. data/lib/factorix/cli/commands/mod/disable.rb +1 -1
  18. data/lib/factorix/cli/commands/mod/download.rb +5 -4
  19. data/lib/factorix/cli/commands/mod/edit.rb +9 -9
  20. data/lib/factorix/cli/commands/mod/enable.rb +1 -1
  21. data/lib/factorix/cli/commands/mod/image/add.rb +2 -2
  22. data/lib/factorix/cli/commands/mod/image/edit.rb +1 -1
  23. data/lib/factorix/cli/commands/mod/image/list.rb +4 -4
  24. data/lib/factorix/cli/commands/mod/install.rb +5 -4
  25. data/lib/factorix/cli/commands/mod/list.rb +7 -7
  26. data/lib/factorix/cli/commands/mod/search.rb +11 -9
  27. data/lib/factorix/cli/commands/mod/settings/dump.rb +3 -3
  28. data/lib/factorix/cli/commands/mod/settings/restore.rb +2 -2
  29. data/lib/factorix/cli/commands/mod/show.rb +20 -20
  30. data/lib/factorix/cli/commands/mod/sync.rb +6 -5
  31. data/lib/factorix/cli/commands/mod/uninstall.rb +1 -1
  32. data/lib/factorix/cli/commands/mod/update.rb +7 -6
  33. data/lib/factorix/cli/commands/mod/upload.rb +6 -6
  34. data/lib/factorix/cli/commands/path.rb +2 -2
  35. data/lib/factorix/cli/commands/version.rb +1 -1
  36. data/lib/factorix/{application.rb → container.rb} +8 -85
  37. data/lib/factorix/dependency/parser.rb +1 -1
  38. data/lib/factorix/http/client.rb +3 -3
  39. data/lib/factorix/info_json.rb +2 -2
  40. data/lib/factorix/mod_list.rb +2 -2
  41. data/lib/factorix/mod_settings.rb +2 -2
  42. data/lib/factorix/runtime/user_configurable.rb +9 -9
  43. data/lib/factorix/service_credential.rb +3 -3
  44. data/lib/factorix/version.rb +1 -1
  45. data/lib/factorix.rb +118 -1
  46. data/sig/factorix/container.rbs +15 -0
  47. data/sig/factorix.rbs +99 -0
  48. metadata +4 -4
  49. data/sig/factorix/application.rbs +0 -86
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0313b61d47124abdc416a512ef8027e0885e26aed91202c422349e43d9ec667
4
- data.tar.gz: 00ce104d88ff8a4a5c1e5ea54afbba614775bf93e1c808c95549b458974883f3
3
+ metadata.gz: 1eb581275b20274cebfa09669eb5f0111ed00f358d5a68947333b5990eb04d56
4
+ data.tar.gz: 2a57a400c94476e860c2647af1e8fb3f845b231b1e377e265bfcc1d1de11d7ef
5
5
  SHA512:
6
- metadata.gz: 7dcd0c9873615cb03f613f7298c535ad4bf2ceb9bdd640b4fcfee3b2cfd31860bc62daaa60568a07149c3ae8a806112bcfc369179e18457bf16c0307f5bc89db
7
- data.tar.gz: 29a513f560d14e1b8bc1fa07e1de2378685de195ae97c79b4993f762af33e245da381c6554eee739494425fa59beb2755a15a2734b917473c9f4baf247171500
6
+ metadata.gz: 9134480ff3cd2675216f6fc9407bb0719467c2488a43449c727294f5871289cb955b6b5a20257314e11d562a60e2eea8edf81bf72c8df739055675142dff8ad5
7
+ data.tar.gz: 15630f358ac2dbe3ae85b3a0798fdea532b7d9cba43352ffc982c74cf6e5eea1a264329f9543c26100618c7d3e45c2b6cc8a69bf5aca203333a2c77157b68571
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-01-18
4
+
5
+ ### Changed
6
+
7
+ - Reorganize configuration and DI container interfaces (#7, #9)
8
+ - `Factorix::Application` renamed to `Factorix::Container` (DI container only)
9
+ - Configuration interface (`config`, `configure`, `load_config`) moved to `Factorix` module
10
+ - Use `Factorix.configure { |c| ... }` instead of `Factorix::Container.configure { |c| ... }`
11
+
12
+ ### Fixed
13
+
14
+ - Fix integer CLI options being parsed as strings after dry-cli 1.4.0 update (#12)
15
+
16
+ ### Deprecated
17
+
18
+ - `Factorix::Application` still works but emits deprecation warnings; will be removed in v1.0
19
+ - DI methods (`[]`, `resolve`, `register`) delegate to `Factorix::Container`
20
+ - Configuration methods (`config`, `configure`) delegate to `Factorix`
21
+
3
22
  ## [0.5.1] - 2026-01-13
4
23
 
5
24
  ### Fixed
data/README.md CHANGED
@@ -65,7 +65,7 @@ $EDITOR ~/.config/factorix/config.rb
65
65
 
66
66
  **Example configuration:**
67
67
  ```ruby
68
- Factorix::Application.configure do |config|
68
+ Factorix.configure do |config|
69
69
  config.runtime.executable_path = "/Applications/Factorio.app/Contents/MacOS/factorio"
70
70
  config.runtime.user_dir = "#{Dir.home}/Library/Application Support/factorio"
71
71
  config.runtime.data_dir = "/Applications/Factorio.app/Contents/data"
@@ -51,7 +51,7 @@ module Factorix
51
51
  return @service_credential if defined?(@service_credential)
52
52
 
53
53
  @service_credential_mutex.synchronize do
54
- @service_credential ||= Application[:service_credential]
54
+ @service_credential ||= Container[:service_credential]
55
55
  end
56
56
  end
57
57
 
@@ -114,7 +114,7 @@ module Factorix
114
114
 
115
115
  URI(value)
116
116
  rescue URI::InvalidURIError => e
117
- Application[:logger].warn("Skipping invalid URI '#{value}': #{e.message}")
117
+ Container[:logger].warn("Skipping invalid URI '#{value}': #{e.message}")
118
118
  nil
119
119
  end
120
120
  end
@@ -145,7 +145,7 @@ module Factorix
145
145
  Release[**r]
146
146
  rescue RangeError => e
147
147
  # Skip releases with invalid version numbers
148
- Application[:logger].warn("Skipping release #{name}@#{r[:version]}: #{e.message}")
148
+ Container[:logger].warn("Skipping release #{name}@#{r[:version]}: #{e.message}")
149
149
  nil
150
150
  end
151
151
  }
@@ -208,7 +208,7 @@ module Factorix
208
208
  return @api_credential if defined?(@api_credential)
209
209
 
210
210
  @api_credential_mutex.synchronize do
211
- @api_credential ||= Application[:api_credential]
211
+ @api_credential ||= Container[:api_credential]
212
212
  end
213
213
  end
214
214
 
@@ -22,7 +22,7 @@ module Factorix
22
22
  # @return [APICredential] new instance with API key from environment
23
23
  # @raise [CredentialError] if API key is not set in environment
24
24
  def self.load
25
- logger = Application["logger"]
25
+ logger = Container["logger"]
26
26
  logger.debug "Loading API credentials from environment"
27
27
 
28
28
  api_key = ENV.fetch(ENV_API_KEY, nil)
@@ -15,7 +15,7 @@ module Factorix
15
15
  # @param base [Class] the class prepending this module
16
16
  def self.prepended(base)
17
17
  base.class_eval do
18
- option :backup_extension, type: :string, default: ".bak", desc: "Backup file extension"
18
+ option :backup_extension, default: ".bak", desc: "Backup file extension"
19
19
  end
20
20
  end
21
21
 
@@ -68,8 +68,8 @@ module Factorix
68
68
  def self.backup_support! = prepend BackupSupport
69
69
 
70
70
  # Common options available to all commands
71
- option :config_path, type: :string, aliases: ["-c"], desc: "Path to configuration file"
72
- option :log_level, type: :string, values: %w[debug info warn error fatal], desc: "Set log level"
71
+ option :config_path, aliases: ["-c"], desc: "Path to configuration file"
72
+ option :log_level, values: %w[debug info warn error fatal], desc: "Set log level"
73
73
  option :quiet, type: :flag, default: false, aliases: ["-q"], desc: "Suppress non-essential output"
74
74
 
75
75
  private def say(message, prefix: "")
@@ -78,7 +78,7 @@ module Factorix
78
78
  resolved_prefix = EMOJI_PREFIXES.fetch(prefix) { prefix.to_s }
79
79
  output = resolved_prefix.empty? ? message : "#{resolved_prefix} #{message}"
80
80
  style = STYLES.fetch(prefix, PLAIN)
81
- puts style[output]
81
+ out.puts style[output]
82
82
  end
83
83
 
84
84
  private def quiet?
@@ -36,7 +36,7 @@ module Factorix
36
36
 
37
37
  option :all, type: :flag, default: false, desc: "Remove all entries"
38
38
  option :expired, type: :flag, default: false, desc: "Remove expired entries only"
39
- option :older_than, type: :string, default: nil, desc: "Remove entries older than AGE (e.g., 30s, 5m, 2h, 7d)"
39
+ option :older_than, default: nil, desc: "Remove entries older than AGE (e.g., 30s, 5m, 2h, 7d)"
40
40
 
41
41
  # Execute the cache evict command
42
42
  #
@@ -95,7 +95,7 @@ module Factorix
95
95
  # @return [Array<Symbol>] resolved cache names
96
96
  # @raise [InvalidArgumentError] if unknown cache name specified
97
97
  private def resolve_cache_names(caches)
98
- all_caches = Application.config.cache.values.keys
98
+ all_caches = Factorix.config.cache.values.keys
99
99
 
100
100
  return all_caches if caches.nil? || caches.empty?
101
101
 
@@ -114,7 +114,7 @@ module Factorix
114
114
  # @param expired [Boolean] remove expired entries only
115
115
  # @return [Hash] eviction result with :count and :size
116
116
  private def evict_cache(name, all:, expired:)
117
- config = Application.config.cache.public_send(name)
117
+ config = Factorix.config.cache.public_send(name)
118
118
  cache_dir = config.dir
119
119
  ttl = config.ttl
120
120
 
@@ -8,7 +8,7 @@ module Factorix
8
8
  module Cache
9
9
  # Display cache statistics
10
10
  #
11
- # This command outputs statistics for all cache stores
11
+ # This command outout.puts statistics for all cache stores
12
12
  # in a human-readable or JSON format.
13
13
  #
14
14
  # @example
@@ -42,18 +42,18 @@ module Factorix
42
42
  logger.debug("Collecting cache statistics")
43
43
 
44
44
  @now = Time.now
45
- cache_names = Application.config.cache.values.keys
45
+ cache_names = Factorix.config.cache.values.keys
46
46
  stats = cache_names.to_h {|name| [name, collect_stats(name)] }
47
47
 
48
48
  if json
49
- puts JSON.pretty_generate(stats)
49
+ out.puts JSON.pretty_generate(stats)
50
50
  else
51
51
  output_text(stats)
52
52
  end
53
53
  end
54
54
 
55
55
  private def collect_stats(name)
56
- config = Application.config.cache.public_send(name)
56
+ config = Factorix.config.cache.public_send(name)
57
57
  cache_dir = config.dir
58
58
 
59
59
  entries = scan_entries(cache_dir, config.ttl)
@@ -142,8 +142,8 @@ module Factorix
142
142
  # @return [void]
143
143
  private def output_text(stats)
144
144
  stats.each_with_index do |(name, data), index|
145
- puts if index > 0
146
- puts "#{name}:"
145
+ out.puts if index > 0
146
+ out.puts "#{name}:"
147
147
  output_cache_stats(data)
148
148
  end
149
149
  end
@@ -153,26 +153,26 @@ module Factorix
153
153
  # @param data [Hash] cache statistics
154
154
  # @return [void]
155
155
  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])}"
156
+ out.puts " Directory: #{data[:directory]}"
157
+ out.puts " TTL: #{format_ttl(data[:ttl])}"
158
+ out.puts " Max file size: #{format_size(data[:max_file_size])}"
159
+ out.puts " Compression: #{format_compression(data[:compression_threshold])}"
160
160
 
161
161
  entries = data[:entries]
162
162
  valid_pct = entries[:total] > 0 ? (Float(entries[:valid]) / entries[:total] * 100) : 0.0
163
- puts " Entries: #{entries[:valid]} / #{entries[:total]} (#{"%.1f" % valid_pct}% valid)"
163
+ out.puts " Entries: #{entries[:valid]} / #{entries[:total]} (#{"%.1f" % valid_pct}% valid)"
164
164
 
165
165
  size = data[:size]
166
- puts " Size: #{format_size(size[:total])} (avg #{format_size(size[:avg])})"
166
+ out.puts " Size: #{format_size(size[:total])} (avg #{format_size(size[:avg])})"
167
167
 
168
168
  age = data[:age]
169
169
  if age[:oldest]
170
- puts " Age: #{format_duration(age[:newest])} - #{format_duration(age[:oldest])} (avg #{format_duration(age[:avg])})"
170
+ out.puts " Age: #{format_duration(age[:newest])} - #{format_duration(age[:oldest])} (avg #{format_duration(age[:avg])})"
171
171
  else
172
- puts " Age: -"
172
+ out.puts " Age: -"
173
173
  end
174
174
 
175
- puts " Stale locks: #{data[:stale_locks]}"
175
+ out.puts " Stale locks: #{data[:stale_locks]}"
176
176
  end
177
177
 
178
178
  # Format TTL value for display
@@ -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,13 +91,13 @@ 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]
100
+ thread_portal = Container[:portal]
101
101
  thread_downloader = thread_portal.mod_download_api.downloader
102
102
 
103
103
  presenter = multi_presenter.register(
@@ -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:)
@@ -29,8 +29,8 @@ module Factorix
29
29
  ]
30
30
 
31
31
  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"
32
+ option :directory, aliases: ["-d"], default: ".", desc: "Download directory"
33
+ option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
34
34
  option :recursive, type: :flag, aliases: ["-r"], default: false, desc: "Include required dependencies recursively"
35
35
 
36
36
  # Execute the download command
@@ -40,7 +40,8 @@ module Factorix
40
40
  # @param jobs [Integer] Number of parallel downloads
41
41
  # @param recursive [Boolean] Include required dependencies recursively
42
42
  # @return [void]
43
- def call(mod_specs:, directory: ".", jobs: 4, recursive: false, **)
43
+ def call(mod_specs:, directory: ".", jobs: "4", recursive: false, **)
44
+ jobs = Integer(jobs)
44
45
  download_dir = Pathname(directory).expand_path
45
46
 
46
47
  raise DirectoryNotFoundError, "Download directory does not exist: #{download_dir}" unless download_dir.exist?
@@ -69,7 +70,7 @@ module Factorix
69
70
  # @param recursive [Boolean] Include dependencies
70
71
  # @return [Array<Hash>] Download targets with MOD info and releases
71
72
  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)
73
+ presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
73
74
 
74
75
  target_infos = fetch_target_mod_info(mod_specs, jobs, presenter)
75
76
 
@@ -19,16 +19,16 @@ module Factorix
19
19
  "some-mod --deprecated # Mark as deprecated"
20
20
  ]
21
21
 
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"
22
+ argument :mod_name, required: true, desc: "MOD name"
23
+ option :description, desc: "Markdown description"
24
+ option :summary, desc: "Brief description"
25
+ option :title, desc: "MOD title"
26
+ option :category, desc: "MOD category"
27
27
  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"
28
+ option :license, desc: "License identifier"
29
+ option :homepage, desc: "Homepage URL"
30
+ option :source_url, desc: "Repository URL"
31
+ option :faq, desc: "FAQ text"
32
32
  option :deprecated, type: :boolean, desc: "Deprecation flag"
33
33
 
34
34
  # 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:)
@@ -18,8 +18,8 @@ module Factorix
18
18
  "some-mod screenshot.png # Add image to MOD"
19
19
  ]
20
20
 
21
- argument :mod_name, type: :string, required: true, desc: "MOD name"
22
- argument :image_file, type: :string, required: true, desc: "Path to image file"
21
+ argument :mod_name, required: true, desc: "MOD name"
22
+ argument :image_file, required: true, desc: "Path to image file"
23
23
 
24
24
  # Execute the add command
25
25
  #
@@ -18,7 +18,7 @@ module Factorix
18
18
  "some-mod abc123 def456 # Set image order (IDs from 'image list')"
19
19
  ]
20
20
 
21
- argument :mod_name, type: :string, required: true, desc: "MOD name"
21
+ argument :mod_name, required: true, desc: "MOD name"
22
22
  argument :image_ids, type: :array, required: true, desc: "Image IDs in desired order"
23
23
 
24
24
  # Execute the edit command
@@ -19,7 +19,7 @@ module Factorix
19
19
  "some-mod --json # List images in JSON format"
20
20
  ]
21
21
 
22
- argument :mod_name, type: :string, required: true, desc: "MOD name"
22
+ argument :mod_name, required: true, desc: "MOD name"
23
23
 
24
24
  option :json, type: :flag, default: false, desc: "Output in JSON format"
25
25
 
@@ -45,7 +45,7 @@ module Factorix
45
45
  end
46
46
 
47
47
  if json
48
- puts JSON.pretty_generate(images)
48
+ out.puts JSON.pretty_generate(images)
49
49
  else
50
50
  output_table(images)
51
51
  end
@@ -60,10 +60,10 @@ module Factorix
60
60
  id_width = [images.map {|i| i[:id].length }.max, 2].max
61
61
  thumb_width = [images.map {|i| i[:thumbnail].length }.max, 9].max
62
62
 
63
- puts "%-#{id_width}s %-#{thumb_width}s %s" % %w[ID THUMBNAIL URL]
63
+ out.puts "%-#{id_width}s %-#{thumb_width}s %s" % %w[ID THUMBNAIL URL]
64
64
 
65
65
  images.each do |image|
66
- puts "%-#{id_width}s %-#{thumb_width}s %s" % [image[:id], image[:thumbnail], image[:url]]
66
+ out.puts "%-#{id_width}s %-#{thumb_width}s %s" % [image[:id], image[:thumbnail], image[:url]]
67
67
  end
68
68
  end
69
69
  end
@@ -34,17 +34,18 @@ module Factorix
34
34
  ]
35
35
 
36
36
  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"
37
+ option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
38
38
 
39
39
  # Execute the install command
40
40
  #
41
41
  # @param mod_specs [Array<String>] MOD specifications
42
42
  # @param jobs [Integer] Number of parallel downloads
43
43
  # @return [void]
44
- def call(mod_specs:, jobs: 4, **)
44
+ def call(mod_specs:, jobs: "4", **)
45
+ jobs = Integer(jobs)
45
46
  # Load current state (without validation to allow fixing issues)
46
47
  mod_list = MODList.load
47
- presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: $stderr)
48
+ presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
48
49
  handler = Progress::ScanHandler.new(presenter)
49
50
  installed_mods = InstalledMOD.all(handler:)
50
51
  graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
@@ -128,7 +129,7 @@ module Factorix
128
129
  # @return [Array<Hash>] Installation targets with MOD info and releases
129
130
  private def plan_installation(mod_specs, graph, jobs)
130
131
  # Create progress presenter for info fetching
131
- presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: $stderr)
132
+ presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
132
133
 
133
134
  # Phase 1: Fetch info for target MODs
134
135
  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
@@ -29,11 +29,11 @@ module Factorix
29
29
  argument :mod_names, type: :array, required: false, default: [], desc: "MOD names to search"
30
30
 
31
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)"
32
+ option :page, default: "1", desc: "Page number"
33
+ option :page_size, default: "25", desc: "Results per page (max 500)"
34
+ option :sort, values: %w[name created_at updated_at], desc: "Sort field"
35
+ option :sort_order, values: %w[asc desc], desc: "Sort order"
36
+ option :version, desc: "Filter by Factorio version (default: installed version)"
37
37
  option :json, type: :flag, default: false, desc: "Output in JSON format"
38
38
 
39
39
  # Execute the search command
@@ -47,7 +47,9 @@ module Factorix
47
47
  # @param version [String, nil] Factorio version filter
48
48
  # @param json [Boolean] Output in JSON format
49
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, **)
50
+ def call(mod_names: [], hide_deprecated: true, page: "1", page_size: "25", sort: nil, sort_order: nil, version: nil, json: false, **)
51
+ page = Integer(page)
52
+ page_size = Integer(page_size)
51
53
  version ||= default_factorio_version
52
54
 
53
55
  mods = portal.list_mods(*mod_names, hide_deprecated: hide_deprecated || nil, page:, page_size:, sort:, sort_order:, version:)
@@ -60,7 +62,7 @@ module Factorix
60
62
  end
61
63
 
62
64
  private def output_json(mods)
63
- puts JSON.pretty_generate(mods.map {|mod| mod_to_hash(mod) })
65
+ out.puts JSON.pretty_generate(mods.map {|mod| mod_to_hash(mod) })
64
66
  end
65
67
 
66
68
  private def mod_to_hash(mod)
@@ -99,10 +101,10 @@ module Factorix
99
101
  headers = %w[NAME TITLE CATEGORY OWNER LATEST]
100
102
  widths = headers.map.with_index {|h, i| [h.length, *rows.map {|r| r[i].to_s.length }].max }
101
103
 
102
- puts format_table_row(headers, widths)
104
+ out.puts format_table_row(headers, widths)
103
105
 
104
106
  rows.each do |row|
105
- puts format_table_row(row, widths)
107
+ out.puts format_table_row(row, widths)
106
108
  end
107
109
 
108
110
  say "#{mods.size} MOD(s) found", prefix: :info
@@ -22,8 +22,8 @@ module Factorix
22
22
  "/path/to/mod-settings.dat -o out.json # Dump specific file"
23
23
  ]
24
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"
25
+ argument :settings_file, required: false, desc: "Path to mod-settings.dat file"
26
+ option :output, aliases: ["-o"], desc: "Output file path"
27
27
 
28
28
  # Execute the dump command
29
29
  #
@@ -43,7 +43,7 @@ module Factorix
43
43
  if output
44
44
  Pathname(output).write(output_string)
45
45
  else
46
- puts output_string
46
+ out.puts output_string
47
47
  end
48
48
  end
49
49
 
@@ -24,8 +24,8 @@ module Factorix
24
24
  " # Restore from stdin"
25
25
  ]
26
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"
27
+ argument :settings_file, required: false, desc: "Path to mod-settings.dat file to write"
28
+ option :input, aliases: ["-i"], desc: "Input file path"
29
29
 
30
30
  # Execute the restore command
31
31
  #