factorix 0.10.0 → 0.11.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f669f6ec36e75ef8f9720e7e09337627d56bc84453233484157643f22a55aa9
4
- data.tar.gz: 2029ef6d075978429ddd533c7ba5aa68eb0c16bf77e9c1802bcbfd159844f767
3
+ metadata.gz: ff57824d3ac0e73ae53844437c03f98bd10537f724a0285986b473b5136559f7
4
+ data.tar.gz: ed520b42e6f293f4e8021a71515f58348882093b634285d96c6d04164550d633
5
5
  SHA512:
6
- metadata.gz: f9a17f3a9a83fdf498555deac57a3d9d99b0b33d77a36c8242e5d78e8e85923ed535f4457bf5976e2e3f1b7209214c84e4eda1f9bf00425657b1c8eb471f34de
7
- data.tar.gz: 0e6c5bfc9e7a09d4ff3529e976b0f7f90cba03d49d2e98ef70ae9eae07031e1c2de90e7f4235dbeb8d68b32d852afa5a674ecc76bcfb048684f9e229454c9c83
6
+ metadata.gz: 36b43e73b76ec6e769ffaa7ec98dbcef83329490a01d6980518786fbb5699661db5e804fc3a0475455196aa0a1f50db2fd42ae233616a17dfef94b4578f71a31
7
+ data.tar.gz: d4f869b4cf329d391d59af1db92082e7acd18c0b6603334949f6fd7ffa22e11cc4536f16f3d0fdfefc1c3eeefa0653a8d0b207dd9eedaac201ad20ffca763fc6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.11.1] - 2026-03-03
4
+
5
+ ### Fixed
6
+
7
+ - Fix `mod sync` incorrectly saving `mod-list.json` and `mod-settings.dat` when nothing changed (#74)
8
+ - Include startup settings changes in the `mod sync` plan and confirmation flow (#74)
9
+
10
+ ## [0.11.0] - 2026-03-03
11
+
12
+ ### Added
13
+
14
+ - Add `blueprint decode` command to decode Factorio blueprint strings to JSON (#76)
15
+ - Add `blueprint encode` command to encode JSON to Factorio blueprint strings (#76)
16
+
17
+ ### Changed
18
+
19
+ - `mod sync` now disables enabled MODs (including expansion MODs) not listed in the save file by default (#70)
20
+ - Add `--keep-unlisted` option to `mod sync` to preserve MODs not listed in the save file (#70)
21
+
3
22
  ## [0.10.0] - 2026-02-21
4
23
 
5
24
  ### Changed
data/README.md CHANGED
@@ -18,6 +18,7 @@ Factorix simplifies Factorio MOD management by providing:
18
18
  - **Installation & Uninstallation**: Install MODs directly from the portal or uninstall existing MODs
19
19
  - **Save File Sync**: Synchronize MOD states and startup settings from Factorio save files
20
20
  - **Settings Management**: Export/import MOD settings in JSON format
21
+ - **Blueprint Conversion**: Decode/encode Factorio blueprint strings to/from JSON
21
22
  - **MOD Portal Integration**: Upload new MODs or update existing ones, edit metadata
22
23
  - **Game Control**: Launch Factorio from the command line
23
24
  - **Game Download**: Download Factorio game files (alpha, expansion, demo, headless)
@@ -17,7 +17,10 @@ _factorix() {
17
17
  local confirmable_opts="-y --yes"
18
18
 
19
19
  # Top-level commands
20
- local commands="version man launch path download mod cache completion"
20
+ local commands="version man launch path download blueprint mod cache completion"
21
+
22
+ # blueprint subcommands
23
+ local blueprint_commands="decode encode"
21
24
 
22
25
  # mod subcommands
23
26
  local mod_commands="changelog check list show enable disable install uninstall update download upload edit search sync image settings"
@@ -66,6 +69,25 @@ _factorix() {
66
69
  fi
67
70
  return
68
71
  ;;
72
+ blueprint)
73
+ if [[ $cword -eq 2 ]]; then
74
+ COMPREPLY=($(compgen -W "$blueprint_commands" -- "$cur"))
75
+ else
76
+ case "${words[2]}" in
77
+ decode|encode)
78
+ if [[ "$cur" == -* ]]; then
79
+ COMPREPLY=($(compgen -W "$global_opts -o --output" -- "$cur"))
80
+ else
81
+ COMPREPLY=($(compgen -f -- "$cur"))
82
+ fi
83
+ ;;
84
+ *)
85
+ COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
86
+ ;;
87
+ esac
88
+ fi
89
+ return
90
+ ;;
69
91
  cache)
70
92
  if [[ $cword -eq 2 ]]; then
71
93
  COMPREPLY=($(compgen -W "$cache_commands" -- "$cur"))
@@ -165,7 +187,7 @@ _factorix() {
165
187
  ;;
166
188
  sync)
167
189
  if [[ "$cur" == -* ]]; then
168
- COMPREPLY=($(compgen -W "$global_opts $confirmable_opts -j --jobs" -- "$cur"))
190
+ COMPREPLY=($(compgen -W "$global_opts $confirmable_opts -j --jobs --keep-unlisted" -- "$cur"))
169
191
  else
170
192
  COMPREPLY=($(compgen -f -X '!*.zip' -- "$cur"))
171
193
  fi
@@ -107,14 +107,25 @@ complete -c factorix -l log-level -d 'Set log level' -xa 'debug info warn error
107
107
  complete -c factorix -s q -l quiet -d 'Suppress non-essential output'
108
108
 
109
109
  # Top-level commands
110
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a version -d 'Display Factorix version'
111
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a man -d 'Display the Factorix manual page'
112
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a launch -d 'Launch Factorio game'
113
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a path -d 'Display Factorio and Factorix paths'
114
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a download -d 'Download Factorio game files'
115
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a mod -d 'MOD management commands'
116
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a cache -d 'Cache management commands'
117
- complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a completion -d 'Generate shell completion script'
110
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a version -d 'Display Factorix version'
111
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a man -d 'Display the Factorix manual page'
112
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a launch -d 'Launch Factorio game'
113
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a path -d 'Display Factorio and Factorix paths'
114
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a download -d 'Download Factorio game files'
115
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a blueprint -d 'Blueprint decode/encode commands'
116
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a mod -d 'MOD management commands'
117
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a cache -d 'Cache management commands'
118
+ complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a completion -d 'Generate shell completion script'
119
+
120
+ # blueprint subcommands
121
+ complete -c factorix -n "__factorix_using_command blueprint" -a decode -d 'Decode a Factorio blueprint string to JSON'
122
+ complete -c factorix -n "__factorix_using_command blueprint" -a encode -d 'Encode JSON to a Factorio blueprint string'
123
+
124
+ # blueprint decode/encode options
125
+ complete -c factorix -n "__factorix_using_subcommand blueprint decode" -s o -l output -d 'Output file path' -rF
126
+ complete -c factorix -n "__factorix_using_subcommand blueprint decode" -rF
127
+ complete -c factorix -n "__factorix_using_subcommand blueprint encode" -s o -l output -d 'Output file path' -rF
128
+ complete -c factorix -n "__factorix_using_subcommand blueprint encode" -rF
118
129
 
119
130
  # launch options
120
131
  complete -c factorix -n "__factorix_using_command launch" -s w -l wait -d 'Wait for the game to finish'
@@ -228,6 +239,7 @@ complete -c factorix -n "__factorix_using_subcommand mod search" -l json -d 'Out
228
239
  # mod sync options
229
240
  complete -c factorix -n "__factorix_using_subcommand mod sync" -s y -l yes -d 'Skip confirmation prompts'
230
241
  complete -c factorix -n "__factorix_using_subcommand mod sync" -s j -l jobs -d 'Number of parallel downloads' -r
242
+ complete -c factorix -n "__factorix_using_subcommand mod sync" -l keep-unlisted -d 'Keep MODs not listed in save file enabled'
231
243
  complete -c factorix -n "__factorix_using_subcommand mod sync" -ra '(__fish_complete_suffix .zip)'
232
244
 
233
245
  # mod changelog subcommands
@@ -40,6 +40,7 @@ _factorix() {
40
40
  'launch:Launch Factorio game'
41
41
  'path:Display Factorio and Factorix paths'
42
42
  'download:Download Factorio game files'
43
+ 'blueprint:Blueprint decode/encode commands'
43
44
  'mod:MOD management commands'
44
45
  'cache:Cache management commands'
45
46
  'completion:Generate shell completion script'
@@ -62,6 +63,9 @@ _factorix() {
62
63
  $global_opts \
63
64
  '--json[Output in JSON format]'
64
65
  ;;
66
+ blueprint)
67
+ _factorix_blueprint
68
+ ;;
65
69
  download)
66
70
  _factorix_download
67
71
  ;;
@@ -79,6 +83,43 @@ _factorix() {
79
83
  esac
80
84
  }
81
85
 
86
+ _factorix_blueprint() {
87
+ local context state state_descr line
88
+ typeset -A opt_args
89
+
90
+ local -a global_opts
91
+ global_opts=(
92
+ '(-c --config-path)'{-c,--config-path}'[Path to configuration file]:config file:_files'
93
+ '--log-level[Set log level]:level:(debug info warn error fatal)'
94
+ '(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
95
+ )
96
+
97
+ _arguments -C \
98
+ '1:subcommand:->subcommand' \
99
+ '*::arg:->args'
100
+
101
+ case $state in
102
+ subcommand)
103
+ local -a subcommands
104
+ subcommands=(
105
+ 'decode:Decode a Factorio blueprint string to JSON'
106
+ 'encode:Encode JSON to a Factorio blueprint string'
107
+ )
108
+ _describe -t subcommands 'blueprint subcommand' subcommands
109
+ ;;
110
+ args)
111
+ case $line[1] in
112
+ decode|encode)
113
+ _arguments \
114
+ $global_opts \
115
+ '(-o --output)'{-o,--output}'[Output file path]:output file:_files' \
116
+ '1:input file:_files'
117
+ ;;
118
+ esac
119
+ ;;
120
+ esac
121
+ }
122
+
82
123
  _factorix_completion() {
83
124
  local -a global_opts
84
125
  global_opts=(
@@ -256,6 +297,7 @@ _factorix_mod() {
256
297
  $global_opts \
257
298
  $confirmable_opts \
258
299
  '(-j --jobs)'{-j,--jobs}'[Number of parallel downloads]:jobs:' \
300
+ '--keep-unlisted[Keep MODs not listed in save file enabled]' \
259
301
  '1:save file:_files -g "*.zip"'
260
302
  ;;
261
303
  changelog)
data/doc/factorix.1 CHANGED
@@ -178,12 +178,16 @@ Disable all MOD(s) except base.
178
178
  Validate MOD dependencies. Reports missing, disabled, or incompatible dependencies.
179
179
  .SS factorix mod sync SAVE_FILE
180
180
  Sync MOD states and startup settings from a save file.
181
+ Enabled MODs not listed in the save file are disabled by default.
181
182
  .TP
182
183
  .BR \-y ", " \-\-yes
183
184
  Skip confirmation prompts.
184
185
  .TP
185
186
  .BR \-j ", " \-\-jobs =\fIVALUE\fR
186
187
  Number of parallel downloads (default: 4).
188
+ .TP
189
+ .B \-\-keep\-unlisted
190
+ Keep MODs not listed in the save file enabled.
187
191
  .SS factorix mod changelog add ENTRY
188
192
  Add an entry to MOD changelog.
189
193
  .TP
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "zlib"
6
+
7
+ module Factorix
8
+ # Represents a Factorio blueprint
9
+ #
10
+ # A blueprint string has the format: version_byte + Base64(zlib(JSON))
11
+ # Only version byte '0' is supported.
12
+ class Blueprint
13
+ # The only supported version byte
14
+ SUPPORTED_VERSION = "0"
15
+ private_constant :SUPPORTED_VERSION
16
+
17
+ # @return [Hash] The blueprint data
18
+ attr_reader :data
19
+
20
+ # Decode a blueprint string into a Blueprint
21
+ #
22
+ # @param string [String] The blueprint string
23
+ # @return [Blueprint]
24
+ # @raise [UnsupportedBlueprintVersionError] if the version byte is not '0'
25
+ # @raise [BlueprintFormatError] if the string is not a valid blueprint
26
+ def self.decode(string)
27
+ version = string[0]
28
+ raise UnsupportedBlueprintVersionError, "Unsupported blueprint version: #{version.inspect}" unless version == SUPPORTED_VERSION
29
+
30
+ compressed = Base64.strict_decode64(string[1..])
31
+ json_string = Zlib::Inflate.inflate(compressed)
32
+ new(JSON.parse(json_string))
33
+ rescue ArgumentError => e
34
+ raise BlueprintFormatError, "Invalid Base64 encoding: #{e.message}"
35
+ rescue Zlib::Error => e
36
+ raise BlueprintFormatError, "Invalid zlib data: #{e.message}"
37
+ rescue JSON::ParserError => e
38
+ raise BlueprintFormatError, "Invalid JSON: #{e.message}"
39
+ end
40
+
41
+ # @param data [Hash] The blueprint data
42
+ def initialize(data)
43
+ @data = data
44
+ end
45
+
46
+ # Encode this blueprint to a blueprint string
47
+ #
48
+ # @return [String]
49
+ def encode
50
+ json_string = JSON.generate(@data)
51
+ compressed = Zlib::Deflate.deflate(json_string, Zlib::BEST_COMPRESSION)
52
+ SUPPORTED_VERSION + Base64.strict_encode64(compressed)
53
+ end
54
+
55
+ # Serialize this blueprint to pretty-printed JSON
56
+ #
57
+ # @return [String]
58
+ def to_json(*)
59
+ JSON.pretty_generate(@data)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class CLI
5
+ module Commands
6
+ module Blueprint
7
+ # Decode a Factorio blueprint string to JSON
8
+ class Decode < Base
9
+ desc "Decode a Factorio blueprint string to JSON"
10
+
11
+ example [
12
+ " # Decode from stdin",
13
+ "blueprint.txt # Decode from file",
14
+ "blueprint.txt -o decoded.json # Decode to file"
15
+ ]
16
+
17
+ argument :file, required: false, desc: "Path to file containing blueprint string (default: stdin)"
18
+ option :output, aliases: ["-o"], desc: "Output file path (default: stdout)"
19
+
20
+ # Execute the decode command
21
+ #
22
+ # @param file [String, nil] Path to file containing blueprint string
23
+ # @param output [String, nil] Output file path
24
+ # @return [void]
25
+ def call(file: nil, output: nil, **)
26
+ blueprint_string = file ? Pathname(file).read.strip : $stdin.read.strip
27
+ blueprint = Factorix::Blueprint.decode(blueprint_string)
28
+ json_string = blueprint.to_json
29
+
30
+ if output
31
+ Pathname(output).write(json_string)
32
+ else
33
+ out.puts json_string
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Factorix
6
+ class CLI
7
+ module Commands
8
+ module Blueprint
9
+ # Encode JSON to a Factorio blueprint string
10
+ class Encode < Base
11
+ desc "Encode JSON to a Factorio blueprint string"
12
+
13
+ example [
14
+ " # Encode from stdin",
15
+ "decoded.json # Encode from file",
16
+ "decoded.json -o blueprint.txt # Encode to file"
17
+ ]
18
+
19
+ argument :file, required: false, desc: "Path to JSON file (default: stdin)"
20
+ option :output, aliases: ["-o"], desc: "Output file path (default: stdout)"
21
+
22
+ # Execute the encode command
23
+ #
24
+ # @param file [String, nil] Path to JSON file
25
+ # @param output [String, nil] Output file path
26
+ # @return [void]
27
+ def call(file: nil, output: nil, **)
28
+ json_string = file ? Pathname(file).read : $stdin.read
29
+ blueprint = Factorix::Blueprint.new(JSON.parse(json_string))
30
+ blueprint_string = blueprint.encode
31
+
32
+ if output
33
+ Pathname(output).write(blueprint_string)
34
+ else
35
+ out.print blueprint_string
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -26,26 +26,27 @@ module Factorix
26
26
  desc "Sync MOD states and startup settings from a save file"
27
27
 
28
28
  example [
29
- "save.zip # Sync MOD(s) from save file",
30
- "-j 8 save.zip # Use 8 parallel downloads"
29
+ "save.zip # Sync MOD(s) from save file",
30
+ "-j 8 save.zip # Use 8 parallel downloads",
31
+ "--keep-unlisted save.zip # Keep MOD(s) not in save file enabled"
31
32
  ]
32
33
 
33
34
  argument :save_file, required: true, desc: "Path to Factorio save file (.zip)"
34
35
  option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
36
+ option :keep_unlisted, type: :flag, default: false, desc: "Keep MOD(s) not listed in save file enabled"
35
37
 
36
38
  # Execute the sync command
37
39
  #
38
40
  # @param save_file [String] Path to save file
39
41
  # @param jobs [Integer] Number of parallel downloads
42
+ # @param keep_unlisted [Boolean] Whether to keep unlisted MODs enabled
40
43
  # @return [void]
41
- def call(save_file:, jobs: "4", **)
44
+ def call(save_file:, jobs: "4", keep_unlisted: false, **)
42
45
  jobs = Integer(jobs)
43
- # Load save file
44
46
  say "Loading save file: #{save_file}", prefix: :info
45
47
  save_data = SaveFile.load(Pathname(save_file))
46
48
  say "Loaded save file (version: #{save_data.version}, MOD(s): #{save_data.mods.size})", prefix: :info
47
49
 
48
- # Load current state
49
50
  mod_list = MODList.load
50
51
  presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
51
52
  handler = Progress::ScanHandler.new(presenter)
@@ -54,48 +55,49 @@ module Factorix
54
55
 
55
56
  raise DirectoryNotFoundError, "MOD directory does not exist: #{runtime.mod_dir}" unless runtime.mod_dir.exist?
56
57
 
57
- # Find MODs that need to be installed
58
+ # Plan phase (no side effects)
58
59
  mods_to_install = find_mods_to_install(save_data.mods, installed_mods)
60
+ install_targets = mods_to_install.any? ? plan_installation(mods_to_install, graph, jobs) : []
61
+ conflict_mods = find_conflict_mods(mod_list, save_data.mods, graph)
62
+ changes = plan_mod_list_changes(mod_list, save_data.mods)
63
+ unlisted_mods = keep_unlisted ? [] : find_unlisted_mods(mod_list, save_data.mods, conflict_mods)
64
+ mod_list_changed = needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods)
65
+ settings_changed = startup_settings_changed?(save_data.startup_settings)
66
+
67
+ # Show combined plan and ask once
68
+ unless mod_list_changed || settings_changed
69
+ say "Nothing to change", prefix: :info
70
+ return
71
+ end
59
72
 
60
- if mods_to_install.any?
61
- say "#{mods_to_install.size} MOD(s) need to be installed", prefix: :info
62
-
63
- # Plan installation
64
- install_targets = plan_installation(mods_to_install, graph, jobs)
65
-
66
- # Show plan
67
- show_install_plan(install_targets)
68
- return unless confirm?("Do you want to install these MOD(s)?")
73
+ show_sync_plan(install_targets, conflict_mods, changes, unlisted_mods, settings_changed)
74
+ return unless confirm?("Do you want to apply these changes?")
69
75
 
70
- # Execute installation
76
+ # Execute phase
77
+ if install_targets.any?
71
78
  execute_installation(install_targets, jobs)
72
79
  say "Installed #{install_targets.size} MOD(s)", prefix: :success
73
- else
74
- say "All MOD(s) from save file are already installed", prefix: :info
75
80
  end
76
81
 
77
- # Resolve conflicts: disable existing MODs that conflict with new ones
78
- resolve_conflicts(mod_list, save_data.mods, graph)
79
-
80
- # Update mod-list.json
81
- update_mod_list(mod_list, save_data.mods)
82
- backup_if_exists(runtime.mod_list_path)
83
- mod_list.save
84
- say "Updated mod-list.json", prefix: :success
82
+ if mod_list_changed
83
+ apply_mod_list_changes(mod_list, conflict_mods, changes, unlisted_mods)
84
+ backup_if_exists(runtime.mod_list_path)
85
+ mod_list.save
86
+ say "Updated mod-list.json", prefix: :success
87
+ end
85
88
 
86
- # Update mod-settings.dat
87
- update_mod_settings(save_data.startup_settings, save_data.version)
88
- say "Updated mod-settings.dat", prefix: :success
89
+ if settings_changed
90
+ update_mod_settings(save_data.startup_settings, save_data.version)
91
+ say "Updated mod-settings.dat", prefix: :success
92
+ end
89
93
 
90
94
  say "Sync completed successfully", prefix: :success
91
95
  end
92
96
 
93
97
  private def find_mods_to_install(save_mods, installed_mods)
94
98
  save_mods.reject do |mod_name, _mod_state|
95
- # Skip base MOD (always installed)
96
99
  next true if mod_name == "base"
97
100
 
98
- # Check if MOD is installed
99
101
  mod = Factorix::MOD[name: mod_name]
100
102
  installed_mods.any? {|installed| installed.mod == mod }
101
103
  end
@@ -108,18 +110,13 @@ module Factorix
108
110
  # @param jobs [Integer] Number of parallel jobs
109
111
  # @return [Array<Hash>] Installation targets with MOD info and releases
110
112
  private def plan_installation(mods_to_install, graph, jobs)
111
- # Create progress presenter for info fetching
112
113
  presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
113
-
114
- # Fetch info for MODs to install
115
114
  target_infos = fetch_target_mod_info(mods_to_install, jobs, presenter)
116
115
 
117
- # Add to graph
118
116
  target_infos.each do |info|
119
117
  graph.add_uninstalled_mod(info[:mod_info], info[:release])
120
118
  end
121
119
 
122
- # Build install targets
123
120
  build_install_targets(target_infos, runtime.mod_dir)
124
121
  end
125
122
 
@@ -155,10 +152,7 @@ module Factorix
155
152
  # @param version [MODVersion] Target version
156
153
  # @return [Hash] {mod_name:, mod_info:, release:, version:}
157
154
  private def fetch_single_mod_info(mod_name, version)
158
- # Fetch full MOD info from portal
159
155
  mod_info = portal.get_mod_full(mod_name)
160
-
161
- # Find the specific version release
162
156
  release = mod_info.releases.find {|r| r.version == version }
163
157
 
164
158
  unless release
@@ -168,100 +162,238 @@ module Factorix
168
162
  {mod_name:, mod_info:, release:, version:}
169
163
  end
170
164
 
171
- # Show the installation plan
172
- #
173
- # @param targets [Array<Hash>] Installation targets
174
- # @return [void]
175
- private def show_install_plan(targets)
176
- say "Planning to install #{targets.size} MOD(s):", prefix: :info
177
- targets.each do |target|
178
- say " - #{target[:mod]}@#{target[:release].version}"
179
- end
180
- end
181
-
182
165
  # Execute the installation
183
166
  #
184
167
  # @param targets [Array<Hash>] Installation targets
185
168
  # @param jobs [Integer] Number of parallel jobs
186
169
  # @return [void]
187
170
  private def execute_installation(targets, jobs)
188
- # Download all MODs
189
171
  download_mods(targets, jobs)
190
172
  end
191
173
 
192
- # Resolve conflicts between save file MODs and existing enabled MODs
174
+ # Find MODs that conflict with enabled MODs from the save file
193
175
  #
194
176
  # @param mod_list [MODList] Current MOD list
195
177
  # @param save_mods [Hash<String, MODState>] MODs from save file
196
178
  # @param graph [Dependency::Graph] Dependency graph
197
- # @return [void]
198
- private def resolve_conflicts(mod_list, save_mods, graph)
179
+ # @return [Array<Hash>] Conflict entries: {mod:, conflicts_with:}
180
+ private def find_conflict_mods(mod_list, save_mods, graph)
181
+ conflicts = []
182
+ seen = Set.new
183
+
199
184
  save_mods.each do |mod_name, mod_state|
200
185
  next unless mod_state.enabled?
201
186
 
202
- mod = Factorix::MOD[name: mod_name]
187
+ save_mod = Factorix::MOD[name: mod_name]
203
188
 
204
- graph.edges_from(mod).each do |edge|
189
+ graph.edges_from(save_mod).each do |edge|
205
190
  next unless edge.incompatible?
206
191
 
207
192
  conflicting_mod = edge.to_mod
208
193
  next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
194
+ next unless seen.add?(conflicting_mod)
209
195
 
210
- mod_list.disable(conflicting_mod)
211
- say "Disabled #{conflicting_mod} (conflicts with #{mod} from save file)", prefix: :warn
212
- logger.debug("Disabled conflicting MOD", mod_name: conflicting_mod.name, conflicts_with: mod.name)
196
+ conflicts << {mod: conflicting_mod, conflicts_with: save_mod}
213
197
  end
214
198
 
215
- graph.edges_to(mod).each do |edge|
199
+ graph.edges_to(save_mod).each do |edge|
216
200
  next unless edge.incompatible?
217
201
 
218
202
  conflicting_mod = edge.from_mod
219
203
  next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
204
+ next unless seen.add?(conflicting_mod)
220
205
 
221
- mod_list.disable(conflicting_mod)
222
- say "Disabled #{conflicting_mod} (conflicts with #{mod} from save file)", prefix: :warn
223
- logger.debug("Disabled conflicting MOD", mod_name: conflicting_mod.name, conflicts_with: mod.name)
206
+ conflicts << {mod: conflicting_mod, conflicts_with: save_mod}
224
207
  end
225
208
  end
209
+
210
+ conflicts
226
211
  end
227
212
 
228
- # Update mod-list.json with MODs from save file
213
+ # Compute changes needed to bring mod-list.json in sync with save file
229
214
  #
230
215
  # @param mod_list [MODList] Current MOD list
231
216
  # @param save_mods [Hash<String, MODState>] MODs from save file
232
- # @return [void]
233
- private def update_mod_list(mod_list, save_mods)
217
+ # @return [Array<Hash>] Change entries: {mod:, action:, ...}
218
+ private def plan_mod_list_changes(mod_list, save_mods)
219
+ changes = []
220
+
234
221
  save_mods.each do |mod_name, mod_state|
235
222
  mod = Factorix::MOD[name: mod_name]
223
+ next if mod.base?
236
224
 
237
- # base MOD: don't update version or enabled state
238
- if mod.base?
239
- logger.debug("Skipping base MOD (no changes allowed)", mod_name:)
240
- next
225
+ if mod_list.exist?(mod)
226
+ changes.concat(plan_existing_mod_changes(mod_list, mod, mod_state))
227
+ else
228
+ # Not in list: add it (silently if disabled)
229
+ changes << {mod:, action: :add, to_enabled: mod_state.enabled?, to_version: mod_state.version}
241
230
  end
231
+ end
232
+
233
+ changes
234
+ end
235
+
236
+ # Plan changes for a single MOD that already exists in mod-list.json
237
+ #
238
+ # @param mod_list [MODList] Current MOD list
239
+ # @param mod [MOD] The MOD to plan changes for
240
+ # @param mod_state [MODState] MOD state from save file
241
+ # @return [Array<Hash>] Change entries for this MOD
242
+ private def plan_existing_mod_changes(mod_list, mod, mod_state)
243
+ current_enabled = mod_list.enabled?(mod)
244
+ return plan_expansion_mod_changes(mod, mod_state, current_enabled) if mod.expansion?
245
+
246
+ current_version = mod_list.version(mod)
247
+ to_enabled = mod_state.enabled?
248
+ to_version = mod_state.version
249
+ enabled_changed = current_enabled != to_enabled
250
+ version_changed = current_version && current_version != to_version
251
+
252
+ if enabled_changed
253
+ action = to_enabled ? :enable : :disable
254
+ [{mod:, action:, from_version: current_version, to_version:, from_enabled: current_enabled}]
255
+ elsif version_changed
256
+ [{mod:, action: :update, from_version: current_version, to_version:, from_enabled: current_enabled}]
257
+ else
258
+ []
259
+ end
260
+ end
261
+
262
+ # Plan enable/disable changes for an expansion MOD
263
+ #
264
+ # @param mod [MOD] The expansion MOD
265
+ # @param mod_state [MODState] MOD state from save file
266
+ # @param current_enabled [Boolean] Current enabled state in mod-list.json
267
+ # @return [Array<Hash>] Change entries (0 or 1 element)
268
+ private def plan_expansion_mod_changes(mod, mod_state, current_enabled)
269
+ if mod_state.enabled? && !current_enabled
270
+ [{mod:, action: :enable}]
271
+ elsif !mod_state.enabled? && current_enabled
272
+ [{mod:, action: :disable}]
273
+ else
274
+ []
275
+ end
276
+ end
277
+
278
+ # Find enabled MODs not listed in the save file (excluding conflict MODs)
279
+ #
280
+ # @param mod_list [MODList] Current MOD list
281
+ # @param save_mods [Hash<String, MODState>] MODs from save file
282
+ # @param conflict_mods [Array<Hash>] Already-planned conflict disables
283
+ # @return [Array<MOD>] MODs to disable
284
+ private def find_unlisted_mods(mod_list, save_mods, conflict_mods)
285
+ conflict_mod_set = Set.new(conflict_mods.map {|c| c[:mod] })
286
+
287
+ mod_list.each_mod.select do |mod|
288
+ !mod.base? &&
289
+ mod_list.enabled?(mod) &&
290
+ !save_mods.key?(mod.name) &&
291
+ !conflict_mod_set.include?(mod)
292
+ end
293
+ end
294
+
295
+ # Show the combined sync plan
296
+ #
297
+ # @param install_targets [Array<Hash>] MODs to install
298
+ # @param conflict_mods [Array<Hash>] MODs to disable due to conflicts
299
+ # @param changes [Array<Hash>] MOD list changes from save file
300
+ # @param unlisted_mods [Array<MOD>] MODs to disable as unlisted
301
+ # @param settings_changed [Boolean] Whether startup settings will be updated
302
+ # @return [void]
303
+ private def show_sync_plan(install_targets, conflict_mods, changes, unlisted_mods, settings_changed)
304
+ say "Planning to sync MOD(s):", prefix: :info
305
+
306
+ if install_targets.any?
307
+ say " Install:"
308
+ install_targets.each {|t| say " - #{t[:mod]}@#{t[:release].version}" }
309
+ end
310
+
311
+ enable_changes = changes.select {|c| c[:action] == :enable }
312
+ if enable_changes.any?
313
+ say " Enable:"
314
+ enable_changes.each {|c| say " - #{c[:mod]}" }
315
+ end
316
+
317
+ disable_changes = changes.select {|c| c[:action] == :disable }
318
+ all_disables = conflict_mods.map {|c| {mod: c[:mod], reason: "(conflicts with #{c[:conflicts_with]})"} } +
319
+ disable_changes.map {|c| {mod: c[:mod], reason: "(disabled in save file)"} } +
320
+ unlisted_mods.map {|m| {mod: m, reason: "(not listed in save file)"} }
321
+ if all_disables.any?
322
+ say " Disable:"
323
+ all_disables.each {|d| say " - #{d[:mod]} #{d[:reason]}" }
324
+ end
325
+
326
+ update_changes = changes.select {|c| c[:action] == :update }
327
+ if update_changes.any?
328
+ say " Update:"
329
+ update_changes.each {|c| say " - #{c[:mod]} (#{c[:from_version]} \u2192 #{c[:to_version]})" }
330
+ end
331
+
332
+ say " Update startup settings" if settings_changed
333
+ end
334
+
335
+ # Apply all mod-list.json changes
336
+ #
337
+ # @param mod_list [MODList] MOD list to modify
338
+ # @param conflict_mods [Array<Hash>] Conflict entries to disable
339
+ # @param changes [Array<Hash>] MOD list changes
340
+ # @param unlisted_mods [Array<MOD>] Unlisted MODs to disable
341
+ # @return [void]
342
+ private def apply_mod_list_changes(mod_list, conflict_mods, changes, unlisted_mods)
343
+ conflict_mods.each do |conflict|
344
+ mod_list.disable(conflict[:mod])
345
+ logger.debug("Disabled conflicting MOD", mod_name: conflict[:mod].name, conflicts_with: conflict[:conflicts_with].name)
346
+ end
347
+
348
+ changes.each {|change| apply_single_change(mod_list, change) }
349
+
350
+ unlisted_mods.each do |mod|
351
+ mod_list.disable(mod)
352
+ logger.debug("Disabled unlisted MOD", mod_name: mod.name)
353
+ end
354
+ end
242
355
 
356
+ # Apply a single change entry to mod-list.json
357
+ #
358
+ # @param mod_list [MODList] MOD list to modify
359
+ # @param change [Hash] Change entry from plan_mod_list_changes
360
+ # @return [void]
361
+ private def apply_single_change(mod_list, change)
362
+ mod = change[:mod]
363
+ case change[:action]
364
+ when :enable
243
365
  if mod_list.exist?(mod)
244
- # expansion MOD: only update enabled state (not version)
245
366
  if mod.expansion?
246
- if mod_state.enabled? && !mod_list.enabled?(mod)
247
- mod_list.enable(mod)
248
- logger.debug("Enabled expansion MOD in mod-list.json", mod_name:)
249
- elsif !mod_state.enabled? && mod_list.enabled?(mod)
250
- mod_list.disable(mod)
251
- logger.debug("Disabled expansion MOD in mod-list.json", mod_name:)
252
- end
367
+ mod_list.enable(mod)
253
368
  else
254
- # Regular MOD: update both version and enabled state
255
- # Remove and re-add to update version
256
369
  mod_list.remove(mod)
257
- mod_list.add(mod, enabled: mod_state.enabled?, version: mod_state.version)
258
- logger.debug("Updated MOD in mod-list.json", mod_name:, version: mod_state.version&.to_s, enabled: mod_state.enabled?)
370
+ mod_list.add(mod, enabled: true, version: change[:to_version])
259
371
  end
260
372
  else
261
- # Add new entry (version from save file)
262
- mod_list.add(mod, enabled: mod_state.enabled?, version: mod_state.version)
263
- logger.debug("Added to mod-list.json", mod_name:, version: mod_state.version&.to_s)
373
+ mod_list.add(mod, enabled: true, version: change[:to_version])
264
374
  end
375
+ logger.debug("Enabled MOD in mod-list.json", mod_name: mod.name)
376
+ when :disable
377
+ if mod_list.exist?(mod)
378
+ if mod.expansion?
379
+ mod_list.disable(mod)
380
+ else
381
+ mod_list.remove(mod)
382
+ mod_list.add(mod, enabled: false, version: change[:to_version])
383
+ end
384
+ else
385
+ mod_list.add(mod, enabled: false, version: change[:to_version])
386
+ end
387
+ logger.debug("Disabled MOD in mod-list.json", mod_name: mod.name)
388
+ when :update
389
+ mod_list.remove(mod)
390
+ mod_list.add(mod, enabled: change[:from_enabled], version: change[:to_version])
391
+ logger.debug("Updated MOD in mod-list.json", mod_name: mod.name, version: change[:to_version]&.to_s)
392
+ when :add
393
+ mod_list.add(mod, enabled: change[:to_enabled], version: change[:to_version])
394
+ logger.debug("Added MOD to mod-list.json", mod_name: mod.name, version: change[:to_version]&.to_s, enabled: change[:to_enabled])
395
+ else
396
+ raise ArgumentError, "Unexpected change action: #{change[:action]}"
265
397
  end
266
398
  end
267
399
 
@@ -271,27 +403,51 @@ module Factorix
271
403
  # @param game_version [GameVersion] Game version from save file
272
404
  # @return [void]
273
405
  private def update_mod_settings(startup_settings, game_version)
274
- # Load existing settings or create new
275
406
  mod_settings = if runtime.mod_settings_path.exist?
276
407
  MODSettings.load(runtime.mod_settings_path)
277
408
  else
278
- # Create new MODSettings with all sections
279
409
  sections = MODSettings::VALID_SECTIONS.to_h {|section_name|
280
410
  [section_name, MODSettings::Section.new(section_name)]
281
411
  }
282
412
  MODSettings.new(game_version, sections)
283
413
  end
284
414
 
285
- # Merge startup settings from save file
286
415
  startup_section = mod_settings["startup"]
287
416
  startup_settings.each do |key, value|
288
417
  startup_section[key] = value
289
418
  end
290
419
 
291
- # Save updated settings
292
420
  backup_if_exists(runtime.mod_settings_path)
293
421
  mod_settings.save(runtime.mod_settings_path)
294
422
  end
423
+
424
+ # Check whether user-visible changes exist that require confirmation
425
+ #
426
+ # @param install_targets [Array<Hash>] MODs to install
427
+ # @param conflict_mods [Array<Hash>] MODs to disable due to conflicts
428
+ # @param changes [Array<Hash>] MOD list changes
429
+ # @param unlisted_mods [Array<MOD>] MODs to disable as unlisted
430
+ # @return [Boolean]
431
+ private def needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods)
432
+ install_targets.any? ||
433
+ conflict_mods.any? ||
434
+ changes.any? {|c| c[:action] != :add || c[:to_enabled] } ||
435
+ unlisted_mods.any?
436
+ end
437
+
438
+ # Check whether startup settings from the save file differ from the current mod-settings.dat
439
+ #
440
+ # @param startup_settings [MODSettings::Section] Startup settings from save file
441
+ # @return [Boolean]
442
+ private def startup_settings_changed?(startup_settings)
443
+ return true unless runtime.mod_settings_path.exist?
444
+
445
+ mod_settings = MODSettings.load(runtime.mod_settings_path)
446
+ startup_section = mod_settings["startup"]
447
+ startup_settings.any? do |key, value|
448
+ startup_section[key] != value
449
+ end
450
+ end
295
451
  end
296
452
  end
297
453
  end
data/lib/factorix/cli.rb CHANGED
@@ -13,6 +13,8 @@ module Factorix
13
13
  class CLI
14
14
  extend Dry::CLI::Registry
15
15
 
16
+ register "blueprint decode", Commands::Blueprint::Decode
17
+ register "blueprint encode", Commands::Blueprint::Encode
16
18
  register "version", Commands::Version
17
19
  register "man", Commands::Man
18
20
  register "launch", Commands::Launch
@@ -71,6 +71,10 @@ module Factorix
71
71
 
72
72
  class ChangelogParseError < FileFormatError; end
73
73
 
74
+ # Blueprint format errors
75
+ class BlueprintFormatError < FileFormatError; end
76
+ class UnsupportedBlueprintVersionError < BlueprintFormatError; end
77
+
74
78
  # =====================================
75
79
  # Domain layer errors
76
80
  # =====================================
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Factorix
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.1"
5
5
  public_constant :VERSION
6
6
  end
@@ -0,0 +1,14 @@
1
+ # RBS type signature file
2
+ # NOTE: Do not include private method definitions in RBS files.
3
+ # Only public interfaces should be documented here.
4
+
5
+ module Factorix
6
+ class Blueprint
7
+ attr_reader data: Hash[String, untyped]
8
+
9
+ def initialize: (Hash[String, untyped] data) -> void
10
+ def self.decode: (String string) -> Blueprint
11
+ def encode: () -> String
12
+ def to_json: (*untyped) -> String
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # RBS type signature file
2
+ # NOTE: Do not include private method definitions in RBS files.
3
+ # Only public interfaces should be documented here.
4
+
5
+ module Factorix
6
+ class CLI
7
+ module Commands
8
+ module Blueprint
9
+ class Decode < Base
10
+ def call: (?file: String?, ?output: String?, **untyped) -> void
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # RBS type signature file
2
+ # NOTE: Do not include private method definitions in RBS files.
3
+ # Only public interfaces should be documented here.
4
+
5
+ module Factorix
6
+ class CLI
7
+ module Commands
8
+ module Blueprint
9
+ class Encode < Base
10
+ def call: (?file: String?, ?output: String?, **untyped) -> void
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -79,6 +79,12 @@ module Factorix
79
79
  class ChangelogParseError < FileFormatError
80
80
  end
81
81
 
82
+ class BlueprintFormatError < FileFormatError
83
+ end
84
+
85
+ class UnsupportedBlueprintVersionError < BlueprintFormatError
86
+ end
87
+
82
88
  # =====================================
83
89
  # Domain layer errors
84
90
  # =====================================
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factorix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OZAWA Sakuro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-21 00:00:00.000000000 Z
11
+ date: 2026-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -250,6 +250,7 @@ files:
250
250
  - lib/factorix/api/release.rb
251
251
  - lib/factorix/api/tag.rb
252
252
  - lib/factorix/api_credential.rb
253
+ - lib/factorix/blueprint.rb
253
254
  - lib/factorix/cache/base.rb
254
255
  - lib/factorix/cache/entry.rb
255
256
  - lib/factorix/cache/file_system.rb
@@ -259,6 +260,8 @@ files:
259
260
  - lib/factorix/cli.rb
260
261
  - lib/factorix/cli/commands/backup_support.rb
261
262
  - lib/factorix/cli/commands/base.rb
263
+ - lib/factorix/cli/commands/blueprint/decode.rb
264
+ - lib/factorix/cli/commands/blueprint/encode.rb
262
265
  - lib/factorix/cli/commands/cache/evict.rb
263
266
  - lib/factorix/cli/commands/cache/stat.rb
264
267
  - lib/factorix/cli/commands/command_wrapper.rb
@@ -362,6 +365,7 @@ files:
362
365
  - sig/factorix/api/release.rbs
363
366
  - sig/factorix/api/tag.rbs
364
367
  - sig/factorix/api_credential.rbs
368
+ - sig/factorix/blueprint.rbs
365
369
  - sig/factorix/cache/base.rbs
366
370
  - sig/factorix/cache/entry.rbs
367
371
  - sig/factorix/cache/file_system.rbs
@@ -370,6 +374,8 @@ files:
370
374
  - sig/factorix/changelog.rbs
371
375
  - sig/factorix/cli.rbs
372
376
  - sig/factorix/cli/commands/base.rbs
377
+ - sig/factorix/cli/commands/blueprint/decode.rbs
378
+ - sig/factorix/cli/commands/blueprint/encode.rbs
373
379
  - sig/factorix/cli/commands/cache/evict.rbs
374
380
  - sig/factorix/cli/commands/cache/stat.rbs
375
381
  - sig/factorix/cli/commands/command_wrapper.rbs