factorix 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/completion/_factorix.bash +202 -0
- data/completion/_factorix.fish +197 -0
- data/completion/_factorix.zsh +376 -0
- data/doc/factorix.1 +377 -0
- data/exe/factorix +20 -0
- data/lib/factorix/api/category.rb +69 -0
- data/lib/factorix/api/image.rb +35 -0
- data/lib/factorix/api/license.rb +71 -0
- data/lib/factorix/api/mod_download_api.rb +66 -0
- data/lib/factorix/api/mod_info.rb +166 -0
- data/lib/factorix/api/mod_management_api.rb +237 -0
- data/lib/factorix/api/mod_portal_api.rb +204 -0
- data/lib/factorix/api/release.rb +49 -0
- data/lib/factorix/api/tag.rb +95 -0
- data/lib/factorix/api.rb +7 -0
- data/lib/factorix/api_credential.rb +54 -0
- data/lib/factorix/application.rb +218 -0
- data/lib/factorix/cache/file_system.rb +307 -0
- data/lib/factorix/cli/commands/backup_support.rb +46 -0
- data/lib/factorix/cli/commands/base.rb +90 -0
- data/lib/factorix/cli/commands/cache/evict.rb +180 -0
- data/lib/factorix/cli/commands/cache/stat.rb +201 -0
- data/lib/factorix/cli/commands/command_wrapper.rb +71 -0
- data/lib/factorix/cli/commands/completion.rb +83 -0
- data/lib/factorix/cli/commands/confirmable.rb +53 -0
- data/lib/factorix/cli/commands/download_support.rb +123 -0
- data/lib/factorix/cli/commands/launch.rb +79 -0
- data/lib/factorix/cli/commands/man.rb +29 -0
- data/lib/factorix/cli/commands/mod/check.rb +99 -0
- data/lib/factorix/cli/commands/mod/disable.rb +188 -0
- data/lib/factorix/cli/commands/mod/download.rb +291 -0
- data/lib/factorix/cli/commands/mod/edit.rb +114 -0
- data/lib/factorix/cli/commands/mod/enable.rb +216 -0
- data/lib/factorix/cli/commands/mod/image/add.rb +47 -0
- data/lib/factorix/cli/commands/mod/image/edit.rb +41 -0
- data/lib/factorix/cli/commands/mod/image/list.rb +74 -0
- data/lib/factorix/cli/commands/mod/install.rb +443 -0
- data/lib/factorix/cli/commands/mod/list.rb +372 -0
- data/lib/factorix/cli/commands/mod/search.rb +134 -0
- data/lib/factorix/cli/commands/mod/settings/dump.rb +88 -0
- data/lib/factorix/cli/commands/mod/settings/restore.rb +101 -0
- data/lib/factorix/cli/commands/mod/show.rb +202 -0
- data/lib/factorix/cli/commands/mod/sync.rb +299 -0
- data/lib/factorix/cli/commands/mod/uninstall.rb +325 -0
- data/lib/factorix/cli/commands/mod/update.rb +222 -0
- data/lib/factorix/cli/commands/mod/upload.rb +90 -0
- data/lib/factorix/cli/commands/path.rb +79 -0
- data/lib/factorix/cli/commands/requires_game_stopped.rb +32 -0
- data/lib/factorix/cli/commands/version.rb +25 -0
- data/lib/factorix/cli.rb +42 -0
- data/lib/factorix/dependency/edge.rb +89 -0
- data/lib/factorix/dependency/entry.rb +124 -0
- data/lib/factorix/dependency/graph/builder.rb +108 -0
- data/lib/factorix/dependency/graph.rb +210 -0
- data/lib/factorix/dependency/list.rb +244 -0
- data/lib/factorix/dependency/mod_version_requirement.rb +73 -0
- data/lib/factorix/dependency/node.rb +60 -0
- data/lib/factorix/dependency/parser.rb +148 -0
- data/lib/factorix/dependency/validation_result.rb +138 -0
- data/lib/factorix/dependency/validator.rb +190 -0
- data/lib/factorix/errors.rb +112 -0
- data/lib/factorix/formatting.rb +56 -0
- data/lib/factorix/game_version.rb +98 -0
- data/lib/factorix/http/cache_decorator.rb +106 -0
- data/lib/factorix/http/cached_response.rb +37 -0
- data/lib/factorix/http/client.rb +187 -0
- data/lib/factorix/http/response.rb +31 -0
- data/lib/factorix/http/retry_decorator.rb +59 -0
- data/lib/factorix/http/retry_strategy.rb +80 -0
- data/lib/factorix/info_json.rb +90 -0
- data/lib/factorix/installed_mod.rb +239 -0
- data/lib/factorix/mod.rb +55 -0
- data/lib/factorix/mod_list.rb +174 -0
- data/lib/factorix/mod_settings.rb +278 -0
- data/lib/factorix/mod_state.rb +34 -0
- data/lib/factorix/mod_version.rb +99 -0
- data/lib/factorix/portal.rb +185 -0
- data/lib/factorix/progress/download_handler.rb +46 -0
- data/lib/factorix/progress/multi_presenter.rb +45 -0
- data/lib/factorix/progress/presenter.rb +67 -0
- data/lib/factorix/progress/presenter_adapter.rb +46 -0
- data/lib/factorix/progress/scan_handler.rb +33 -0
- data/lib/factorix/progress/upload_handler.rb +33 -0
- data/lib/factorix/runtime/base.rb +233 -0
- data/lib/factorix/runtime/linux.rb +32 -0
- data/lib/factorix/runtime/mac_os.rb +53 -0
- data/lib/factorix/runtime/user_configurable.rb +69 -0
- data/lib/factorix/runtime/windows.rb +85 -0
- data/lib/factorix/runtime/wsl.rb +118 -0
- data/lib/factorix/runtime.rb +32 -0
- data/lib/factorix/save_file.rb +178 -0
- data/lib/factorix/ser_des/deserializer.rb +198 -0
- data/lib/factorix/ser_des/serializer.rb +231 -0
- data/lib/factorix/ser_des/signed_integer.rb +63 -0
- data/lib/factorix/ser_des/unsigned_integer.rb +65 -0
- data/lib/factorix/service_credential.rb +127 -0
- data/lib/factorix/transfer/downloader.rb +162 -0
- data/lib/factorix/transfer/uploader.rb +232 -0
- data/lib/factorix/version.rb +6 -0
- data/lib/factorix.rb +38 -0
- data/sig/dry/auto_inject.rbs +15 -0
- data/sig/dry/cli.rbs +19 -0
- data/sig/dry/configurable.rbs +13 -0
- data/sig/dry/core/container.rbs +17 -0
- data/sig/dry/events/publisher.rbs +22 -0
- data/sig/dry/logger.rbs +16 -0
- data/sig/factorix/api/category.rbs +15 -0
- data/sig/factorix/api/image.rbs +15 -0
- data/sig/factorix/api/license.rbs +20 -0
- data/sig/factorix/api/mod_download_api.rbs +18 -0
- data/sig/factorix/api/mod_info.rbs +67 -0
- data/sig/factorix/api/mod_management_api.rbs +25 -0
- data/sig/factorix/api/mod_portal_api.rbs +31 -0
- data/sig/factorix/api/release.rbs +27 -0
- data/sig/factorix/api/tag.rbs +15 -0
- data/sig/factorix/api.rbs +8 -0
- data/sig/factorix/api_credential.rbs +17 -0
- data/sig/factorix/application.rbs +86 -0
- data/sig/factorix/cache/file_system.rbs +35 -0
- data/sig/factorix/cli/commands/base.rbs +13 -0
- data/sig/factorix/cli/commands/cache/evict.rbs +17 -0
- data/sig/factorix/cli/commands/cache/stat.rbs +17 -0
- data/sig/factorix/cli/commands/command_wrapper.rbs +13 -0
- data/sig/factorix/cli/commands/completion/zsh.rbs +15 -0
- data/sig/factorix/cli/commands/confirmable.rbs +12 -0
- data/sig/factorix/cli/commands/download_support.rbs +12 -0
- data/sig/factorix/cli/commands/launch.rbs +15 -0
- data/sig/factorix/cli/commands/mod/check.rbs +18 -0
- data/sig/factorix/cli/commands/mod/disable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/download.rbs +18 -0
- data/sig/factorix/cli/commands/mod/edit.rbs +30 -0
- data/sig/factorix/cli/commands/mod/enable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/image/add.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/edit.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/list.rbs +19 -0
- data/sig/factorix/cli/commands/mod/install.rbs +19 -0
- data/sig/factorix/cli/commands/mod/list.rbs +30 -0
- data/sig/factorix/cli/commands/mod/search.rbs +18 -0
- data/sig/factorix/cli/commands/mod/settings/dump.rbs +17 -0
- data/sig/factorix/cli/commands/mod/settings/restore.rbs +17 -0
- data/sig/factorix/cli/commands/mod/sync.rbs +19 -0
- data/sig/factorix/cli/commands/mod/uninstall.rbs +20 -0
- data/sig/factorix/cli/commands/mod/update.rbs +19 -0
- data/sig/factorix/cli/commands/mod/upload.rbs +24 -0
- data/sig/factorix/cli/commands/path.rbs +18 -0
- data/sig/factorix/cli/commands/requires_game_stopped.rbs +13 -0
- data/sig/factorix/cli/commands/version.rbs +13 -0
- data/sig/factorix/cli.rbs +11 -0
- data/sig/factorix/dependency/edge.rbs +32 -0
- data/sig/factorix/dependency/entry.rbs +30 -0
- data/sig/factorix/dependency/graph/builder.rbs +17 -0
- data/sig/factorix/dependency/graph.rbs +39 -0
- data/sig/factorix/dependency/list.rbs +69 -0
- data/sig/factorix/dependency/mod_version_requirement.rbs +18 -0
- data/sig/factorix/dependency/node.rbs +24 -0
- data/sig/factorix/dependency/parser.rbs +11 -0
- data/sig/factorix/dependency/validation_result.rbs +56 -0
- data/sig/factorix/dependency/validator.rbs +13 -0
- data/sig/factorix/errors.rbs +132 -0
- data/sig/factorix/formatting.rbs +8 -0
- data/sig/factorix/game_version.rbs +24 -0
- data/sig/factorix/http/cache_decorator.rbs +64 -0
- data/sig/factorix/http/client.rbs +55 -0
- data/sig/factorix/http/response.rbs +28 -0
- data/sig/factorix/http/retry_decorator.rbs +44 -0
- data/sig/factorix/http/retry_strategy.rbs +42 -0
- data/sig/factorix/info_json.rbs +19 -0
- data/sig/factorix/installed_mod.rbs +34 -0
- data/sig/factorix/mod.rbs +20 -0
- data/sig/factorix/mod_list.rbs +44 -0
- data/sig/factorix/mod_settings.rbs +47 -0
- data/sig/factorix/mod_state.rbs +18 -0
- data/sig/factorix/mod_version.rbs +23 -0
- data/sig/factorix/portal.rbs +37 -0
- data/sig/factorix/progress/download_handler.rbs +19 -0
- data/sig/factorix/progress/multi_presenter.rbs +15 -0
- data/sig/factorix/progress/presenter.rbs +17 -0
- data/sig/factorix/progress/presenter_adapter.rbs +17 -0
- data/sig/factorix/progress/scan_handler.rbs +16 -0
- data/sig/factorix/progress/upload_handler.rbs +17 -0
- data/sig/factorix/runtime/base.rbs +45 -0
- data/sig/factorix/runtime/linux.rbs +15 -0
- data/sig/factorix/runtime/mac_os.rbs +15 -0
- data/sig/factorix/runtime/user_configurable.rbs +13 -0
- data/sig/factorix/runtime/windows.rbs +23 -0
- data/sig/factorix/runtime/wsl.rbs +19 -0
- data/sig/factorix/runtime.rbs +9 -0
- data/sig/factorix/save_file.rbs +40 -0
- data/sig/factorix/ser_des/deserializer.rbs +49 -0
- data/sig/factorix/ser_des/serializer.rbs +45 -0
- data/sig/factorix/ser_des/signed_integer.rbs +37 -0
- data/sig/factorix/ser_des/unsigned_integer.rbs +37 -0
- data/sig/factorix/service_credential.rbs +19 -0
- data/sig/factorix/transfer/downloader.rbs +15 -0
- data/sig/factorix/transfer/uploader.rbs +21 -0
- data/sig/factorix.rbs +9 -0
- data/sig/tty/progressbar.rbs +18 -0
- metadata +431 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Mixin for commands that require user confirmation
|
|
7
|
+
#
|
|
8
|
+
# This module provides:
|
|
9
|
+
# - --yes option to skip confirmation prompts
|
|
10
|
+
# - confirm? method to ask for user confirmation
|
|
11
|
+
#
|
|
12
|
+
# Prepend this module to commands that need confirmation
|
|
13
|
+
# (e.g., enable, disable, install, uninstall)
|
|
14
|
+
module Confirmable
|
|
15
|
+
# Hook called when this module is prepended to a class
|
|
16
|
+
# @param base [Class] the class prepending this module
|
|
17
|
+
def self.prepended(base)
|
|
18
|
+
base.class_eval do
|
|
19
|
+
option :yes, type: :flag, default: false, aliases: ["-y"], desc: "Skip confirmation prompts"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Store the --yes flag for use in confirm?
|
|
24
|
+
# @param options [Hash] command options
|
|
25
|
+
def call(**options)
|
|
26
|
+
@yes = options[:yes]
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Ask for user confirmation
|
|
31
|
+
#
|
|
32
|
+
# @param message [String] confirmation message to display
|
|
33
|
+
# @return [Boolean] true if user confirms, false otherwise
|
|
34
|
+
# @raise [InvalidOperationError] if in quiet mode without --yes flag
|
|
35
|
+
private def confirm?(message="Do you want to continue?")
|
|
36
|
+
# --yes flag skips confirmation
|
|
37
|
+
return true if @yes
|
|
38
|
+
|
|
39
|
+
# Cannot prompt in quiet mode
|
|
40
|
+
if quiet?
|
|
41
|
+
raise InvalidOperationError, "Cannot prompt for confirmation in quiet mode. Use --yes to proceed automatically."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
print "#{message} [y/N] "
|
|
45
|
+
response = $stdin.gets&.strip&.downcase
|
|
46
|
+
|
|
47
|
+
# Only explicit y or yes means yes (default is no for safety)
|
|
48
|
+
response == "y" || response == "yes"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/executor/fixed_thread_pool"
|
|
4
|
+
require "concurrent/future"
|
|
5
|
+
|
|
6
|
+
module Factorix
|
|
7
|
+
class CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# Provides common download functionality for MOD commands
|
|
10
|
+
#
|
|
11
|
+
# This module extracts the common download logic used across
|
|
12
|
+
# Download, Install, and Sync commands.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# class Install < Base
|
|
16
|
+
# include DownloadSupport
|
|
17
|
+
#
|
|
18
|
+
# def call(mod_specs:, **options)
|
|
19
|
+
# # ... build targets ...
|
|
20
|
+
# download_mods(targets, jobs)
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
module DownloadSupport
|
|
24
|
+
# Parse MOD specification into mod and version
|
|
25
|
+
#
|
|
26
|
+
# @param mod_spec [String] MOD specification (name@version or name@latest or name)
|
|
27
|
+
# @return [Hash] {mod:, version:} where version is MODVersion or :latest
|
|
28
|
+
private def parse_mod_spec(mod_spec)
|
|
29
|
+
parts = mod_spec.split("@", 2)
|
|
30
|
+
mod = Factorix::MOD[name: parts[0]]
|
|
31
|
+
version_spec = parts[1]
|
|
32
|
+
version = case version_spec
|
|
33
|
+
when nil, "", "latest" then :latest
|
|
34
|
+
else MODVersion.from_string(version_spec)
|
|
35
|
+
end
|
|
36
|
+
{mod:, version:}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Find the appropriate release for a version
|
|
40
|
+
#
|
|
41
|
+
# @param mod_info [API::MODInfo] MOD information
|
|
42
|
+
# @param version [MODVersion, Symbol] Version or :latest
|
|
43
|
+
# @return [API::Release, nil] The release, or nil if not found
|
|
44
|
+
private def find_release(mod_info, version)
|
|
45
|
+
if version == :latest
|
|
46
|
+
mod_info.releases.max_by(&:released_at)
|
|
47
|
+
else
|
|
48
|
+
mod_info.releases.find {|r| r.version == version }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Find a release compatible with a version requirement
|
|
53
|
+
#
|
|
54
|
+
# @param mod_info [API::MODInfo] MOD information
|
|
55
|
+
# @param version_requirement [Dependency::MODVersionRequirement, nil] Version requirement
|
|
56
|
+
# @return [API::Release, nil] Compatible release or nil
|
|
57
|
+
private def find_compatible_release(mod_info, version_requirement)
|
|
58
|
+
return mod_info.releases.max_by(&:released_at) if version_requirement.nil?
|
|
59
|
+
|
|
60
|
+
compatible_releases = mod_info.releases.select {|r| version_requirement.satisfied_by?(r.version) }
|
|
61
|
+
|
|
62
|
+
compatible_releases.max_by(&:released_at)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build install targets from MOD infos
|
|
66
|
+
#
|
|
67
|
+
# @param mod_infos [Array<Hash>] MOD infos, each containing:
|
|
68
|
+
# - :mod [Factorix::MOD] or :mod_name [String] MOD identifier
|
|
69
|
+
# - :mod_info [API::MODInfo] MOD information
|
|
70
|
+
# - :release [API::Release] Release to install
|
|
71
|
+
# @param output_dir [Pathname] Output directory for MOD files
|
|
72
|
+
# @return [Array<Hash>] Install targets
|
|
73
|
+
private def build_install_targets(mod_infos, output_dir)
|
|
74
|
+
mod_infos.map {|info|
|
|
75
|
+
{
|
|
76
|
+
mod: info[:mod] || Factorix::MOD[name: info[:mod_name]],
|
|
77
|
+
mod_info: info[:mod_info],
|
|
78
|
+
release: info[:release],
|
|
79
|
+
output_path: output_dir / info[:release].file_name,
|
|
80
|
+
category: info[:mod_info].category
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Download MODs in parallel
|
|
86
|
+
#
|
|
87
|
+
# @param targets [Array<Hash>] Download targets, each containing:
|
|
88
|
+
# - :mod [Factorix::MOD] MOD object
|
|
89
|
+
# - :release [API::Release] Release to download
|
|
90
|
+
# - :output_path [Pathname] Output file path
|
|
91
|
+
# @param jobs [Integer] Number of parallel downloads
|
|
92
|
+
# @return [void]
|
|
93
|
+
private def download_mods(targets, jobs)
|
|
94
|
+
multi_presenter = Progress::MultiPresenter.new(title: "\u{1F4E5}\u{FE0E} Downloads")
|
|
95
|
+
|
|
96
|
+
pool = Concurrent::FixedThreadPool.new(jobs)
|
|
97
|
+
|
|
98
|
+
futures = targets.map {|target|
|
|
99
|
+
Concurrent::Future.execute(executor: pool) do
|
|
100
|
+
thread_portal = Application[:portal]
|
|
101
|
+
thread_downloader = thread_portal.mod_download_api.downloader
|
|
102
|
+
|
|
103
|
+
presenter = multi_presenter.register(
|
|
104
|
+
target[:mod].name,
|
|
105
|
+
title: target[:release].file_name
|
|
106
|
+
)
|
|
107
|
+
handler = Progress::DownloadHandler.new(presenter)
|
|
108
|
+
|
|
109
|
+
thread_downloader.subscribe(handler)
|
|
110
|
+
thread_portal.download_mod(target[:release], target[:output_path])
|
|
111
|
+
thread_downloader.unsubscribe(handler)
|
|
112
|
+
end
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
futures.each(&:wait!)
|
|
116
|
+
ensure
|
|
117
|
+
pool&.shutdown
|
|
118
|
+
pool&.wait_for_termination
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Launch Factorio game
|
|
7
|
+
#
|
|
8
|
+
# This command launches the Factorio game executable with optional arguments.
|
|
9
|
+
# By default, the game is launched asynchronously (in the background), but certain
|
|
10
|
+
# options like --help and --dump-* are automatically detected and run synchronously.
|
|
11
|
+
class Launch < Base
|
|
12
|
+
require_game_stopped!
|
|
13
|
+
# Game options that require synchronous execution
|
|
14
|
+
#
|
|
15
|
+
# These options output information and exit immediately, so we should
|
|
16
|
+
# wait for them to complete rather than running them in the background.
|
|
17
|
+
SYNCHRONOUS_OPTIONS = %w[
|
|
18
|
+
--dump-data
|
|
19
|
+
--dump-icon-sprites
|
|
20
|
+
--dump-prototype-locale
|
|
21
|
+
--help
|
|
22
|
+
--version
|
|
23
|
+
].freeze
|
|
24
|
+
private_constant :SYNCHRONOUS_OPTIONS
|
|
25
|
+
|
|
26
|
+
# @!parse
|
|
27
|
+
# # @return [Runtime::Base]
|
|
28
|
+
# attr_reader :runtime
|
|
29
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
30
|
+
# attr_reader :logger
|
|
31
|
+
include Import[:runtime, :logger]
|
|
32
|
+
|
|
33
|
+
desc "Launch Factorio game"
|
|
34
|
+
|
|
35
|
+
example [
|
|
36
|
+
" # Launch Factorio",
|
|
37
|
+
"-- --help # Show Factorio help",
|
|
38
|
+
"-- --benchmark save.zip # Run benchmark"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
option :wait, type: :flag, default: false, aliases: ["-w"], desc: "Wait for the game to finish"
|
|
42
|
+
|
|
43
|
+
# Execute the launch command
|
|
44
|
+
#
|
|
45
|
+
# @param wait [Boolean] whether to wait for the game to finish
|
|
46
|
+
# @param args [Array<String>] additional arguments to pass to Factorio
|
|
47
|
+
# @return [void]
|
|
48
|
+
def call(wait: false, args: [], **)
|
|
49
|
+
logger.info("Launching Factorio", args:)
|
|
50
|
+
|
|
51
|
+
async = args.none? {|arg| SYNCHRONOUS_OPTIONS.include?(arg) }
|
|
52
|
+
|
|
53
|
+
runtime.launch(*args, async:)
|
|
54
|
+
logger.info("Factorio launched successfully", async:)
|
|
55
|
+
|
|
56
|
+
return unless async && wait
|
|
57
|
+
|
|
58
|
+
logger.debug("Waiting for game to start")
|
|
59
|
+
wait_while { !runtime.running? }
|
|
60
|
+
logger.debug("Game started, waiting for termination")
|
|
61
|
+
wait_while { runtime.running? }
|
|
62
|
+
logger.info("Game terminated")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Wait while a condition is true
|
|
66
|
+
#
|
|
67
|
+
# @yield the condition to check
|
|
68
|
+
# @return [void]
|
|
69
|
+
private def wait_while
|
|
70
|
+
loop do
|
|
71
|
+
break unless yield
|
|
72
|
+
|
|
73
|
+
sleep 1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Display the Factorix manual page
|
|
7
|
+
#
|
|
8
|
+
# This command opens the man page for factorix using the system's man command.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# $ factorix man
|
|
12
|
+
class Man < Base
|
|
13
|
+
desc "Display the Factorix manual page"
|
|
14
|
+
|
|
15
|
+
# Execute the man command
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
# @raise [CommandNotFoundError] if man command is not available
|
|
19
|
+
def call(**)
|
|
20
|
+
system("command -v man > /dev/null 2>&1")
|
|
21
|
+
raise CommandNotFoundError, "man command is not available on this system" unless $?.success?
|
|
22
|
+
|
|
23
|
+
man_page = File.expand_path("../../../../doc/factorix.1", __dir__)
|
|
24
|
+
exec "man", man_page
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module MOD
|
|
7
|
+
# Validate MOD dependencies without making changes
|
|
8
|
+
class Check < Base
|
|
9
|
+
# @!parse
|
|
10
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
11
|
+
# attr_reader :logger
|
|
12
|
+
# # @return [Factorix::Runtime]
|
|
13
|
+
# attr_reader :runtime
|
|
14
|
+
include Import[:logger, :runtime]
|
|
15
|
+
|
|
16
|
+
desc "Validate MOD dependencies"
|
|
17
|
+
|
|
18
|
+
example [
|
|
19
|
+
" # Validate all MOD dependencies"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Execute the check command
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def call(**)
|
|
26
|
+
mod_list = MODList.load
|
|
27
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: $stderr)
|
|
28
|
+
handler = Progress::ScanHandler.new(presenter)
|
|
29
|
+
installed_mods = InstalledMOD.all(handler:)
|
|
30
|
+
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
31
|
+
|
|
32
|
+
validator = Dependency::Validator.new(graph:, mod_list:, installed_mods:)
|
|
33
|
+
result = validator.validate
|
|
34
|
+
|
|
35
|
+
display_result(result, graph)
|
|
36
|
+
|
|
37
|
+
raise ValidationError, "MOD dependency validation failed" unless result.valid?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private def display_result(result, graph)
|
|
41
|
+
say "Validating MOD dependencies...", prefix: :info
|
|
42
|
+
|
|
43
|
+
if result.valid? && !result.warnings?
|
|
44
|
+
display_success_messages
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
display_warnings(result) if result.warnings?
|
|
48
|
+
display_errors(result) if result.errors?
|
|
49
|
+
display_suggestions(result) if result.suggestions?
|
|
50
|
+
|
|
51
|
+
display_summary(result, graph)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private def display_success_messages
|
|
55
|
+
say "All enabled MOD(s) have their required dependencies satisfied", prefix: :success
|
|
56
|
+
say "No circular dependencies detected", prefix: :success
|
|
57
|
+
say "No conflicting MOD(s) are enabled simultaneously", prefix: :success
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private def display_warnings(result)
|
|
61
|
+
say "Warnings:", prefix: :warn
|
|
62
|
+
result.warnings.each do |warning|
|
|
63
|
+
say " - #{warning.message}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private def display_errors(result)
|
|
68
|
+
say "Errors:", prefix: :error
|
|
69
|
+
result.errors.each do |error|
|
|
70
|
+
say " - #{error.message}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private def display_suggestions(result)
|
|
75
|
+
say "Suggestions:", prefix: :info
|
|
76
|
+
result.suggestions.each do |suggestion|
|
|
77
|
+
say " - #{suggestion.message}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private def display_summary(result, graph)
|
|
82
|
+
enabled_count = graph.nodes.count(&:enabled?)
|
|
83
|
+
parts = ["#{enabled_count} enabled MOD#{"s" unless enabled_count == 1}"]
|
|
84
|
+
|
|
85
|
+
if result.errors?
|
|
86
|
+
parts << "#{result.errors.size} error#{"s" unless result.errors.size == 1}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if result.warnings?
|
|
90
|
+
parts << "#{result.warnings.size} warning#{"s" unless result.warnings.size == 1}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
say "Summary: #{parts.join(", ")}", prefix: :info
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module MOD
|
|
7
|
+
# Disable MODs in mod-list.json with reverse dependency resolution
|
|
8
|
+
class Disable < Base
|
|
9
|
+
confirmable!
|
|
10
|
+
require_game_stopped!
|
|
11
|
+
backup_support!
|
|
12
|
+
|
|
13
|
+
# @!parse
|
|
14
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
15
|
+
# attr_reader :logger
|
|
16
|
+
# # @return [Factorix::Runtime]
|
|
17
|
+
# attr_reader :runtime
|
|
18
|
+
include Import[:logger, :runtime]
|
|
19
|
+
|
|
20
|
+
desc "Disable MOD(s) in mod-list.json (recursively disables dependent MOD(s))"
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
"some-mod # Disable single MOD",
|
|
24
|
+
"mod-a mod-b # Disable multiple MOD(s)",
|
|
25
|
+
"--all # Disable all MOD(s) except base"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
argument :mod_names, type: :array, required: false, desc: "MOD names to disable"
|
|
29
|
+
|
|
30
|
+
option :all, type: :flag, default: false, desc: "Disable all MOD(s) (except base)"
|
|
31
|
+
|
|
32
|
+
# Execute the disable command
|
|
33
|
+
#
|
|
34
|
+
# @param mod_names [Array<String>] MOD names to disable
|
|
35
|
+
# @param all [Boolean] Whether to disable all MODs
|
|
36
|
+
# @return [void]
|
|
37
|
+
def call(mod_names: [], all: false, **)
|
|
38
|
+
validate_arguments(mod_names, all)
|
|
39
|
+
|
|
40
|
+
# Without validation to allow fixing issues
|
|
41
|
+
mod_list = MODList.load
|
|
42
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: $stderr)
|
|
43
|
+
handler = Progress::ScanHandler.new(presenter)
|
|
44
|
+
installed_mods = InstalledMOD.all(handler:)
|
|
45
|
+
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
46
|
+
|
|
47
|
+
target_mods = if all
|
|
48
|
+
plan_disable_all(graph)
|
|
49
|
+
else
|
|
50
|
+
mod_names.map {|name| Factorix::MOD[name:] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
validate_target_mods(target_mods, graph)
|
|
54
|
+
mods_to_disable = plan_with_dependents(target_mods, graph)
|
|
55
|
+
|
|
56
|
+
show_plan(mods_to_disable)
|
|
57
|
+
return if mods_to_disable.empty?
|
|
58
|
+
return unless confirm?("Do you want to disable these MOD(s)?")
|
|
59
|
+
|
|
60
|
+
execute_plan(mods_to_disable, mod_list)
|
|
61
|
+
backup_if_exists(runtime.mod_list_path)
|
|
62
|
+
mod_list.save
|
|
63
|
+
say "Disabled #{mods_to_disable.size} MOD(s)", prefix: :success
|
|
64
|
+
say "Saved mod-list.json", prefix: :success
|
|
65
|
+
logger.debug("Saved mod-list.json")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Validate command arguments
|
|
69
|
+
#
|
|
70
|
+
# @param mod_names [Array<String>] MOD names from argument
|
|
71
|
+
# @param all [Boolean] Whether --all option is specified
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @raise [InvalidArgumentError] if arguments are invalid
|
|
74
|
+
private def validate_arguments(mod_names, all)
|
|
75
|
+
if all && mod_names.any?
|
|
76
|
+
raise InvalidArgumentError, "Cannot specify MOD names with --all option"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return if all || mod_names.any?
|
|
80
|
+
|
|
81
|
+
raise InvalidArgumentError, "Must specify MOD names or use --all option"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Plan which MODs to disable when --all is specified
|
|
85
|
+
#
|
|
86
|
+
# @param graph [Factorix::Dependency::Graph] Dependency graph
|
|
87
|
+
# @return [Array<Factorix::MOD>] MODs to disable (all except base)
|
|
88
|
+
private def plan_disable_all(graph)
|
|
89
|
+
graph.nodes.filter_map do |node|
|
|
90
|
+
mod = node.mod
|
|
91
|
+
next if mod.base?
|
|
92
|
+
next unless node.enabled?
|
|
93
|
+
|
|
94
|
+
mod
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Validate that all target MODs can be disabled
|
|
99
|
+
#
|
|
100
|
+
# @param target_mods [Array<Factorix::MOD>] MODs to validate
|
|
101
|
+
# @param graph [Factorix::Dependency::Graph] Dependency graph
|
|
102
|
+
# @return [void]
|
|
103
|
+
# @raise [InvalidOperationError] if any MOD cannot be disabled
|
|
104
|
+
private def validate_target_mods(target_mods, graph)
|
|
105
|
+
target_mods.each do |mod|
|
|
106
|
+
raise InvalidOperationError, "Cannot disable base MOD" if mod.base?
|
|
107
|
+
|
|
108
|
+
unless graph.node?(mod)
|
|
109
|
+
say "MOD not installed, skipping: #{mod}", prefix: :warn
|
|
110
|
+
logger.debug("MOD not installed", mod_name: mod.name)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Plan disable with automatic dependent resolution
|
|
116
|
+
#
|
|
117
|
+
# Finds all enabled MODs that depend on the target MODs recursively.
|
|
118
|
+
#
|
|
119
|
+
# @param target_mods [Array<Factorix::MOD>] MODs to disable
|
|
120
|
+
# @param graph [Factorix::Dependency::Graph] Dependency graph
|
|
121
|
+
# @return [Array<Factorix::MOD>] MODs to disable (including dependents)
|
|
122
|
+
private def plan_with_dependents(target_mods, graph)
|
|
123
|
+
mods_to_disable = Set.new
|
|
124
|
+
to_process = target_mods.dup
|
|
125
|
+
|
|
126
|
+
while (mod = to_process.shift)
|
|
127
|
+
node = graph.node(mod)
|
|
128
|
+
|
|
129
|
+
unless node
|
|
130
|
+
logger.debug("MOD not installed", mod_name: mod.name)
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
unless node.enabled?
|
|
135
|
+
logger.debug("MOD already disabled", mod_name: mod.name)
|
|
136
|
+
next
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
next if mods_to_disable.include?(mod)
|
|
140
|
+
|
|
141
|
+
dependents = graph.find_enabled_dependents(mod)
|
|
142
|
+
|
|
143
|
+
dependents.each do |dependent_mod|
|
|
144
|
+
logger.debug("Found dependent MOD", dependent: dependent_mod.name, dependency: mod.name)
|
|
145
|
+
to_process << dependent_mod unless mods_to_disable.include?(dependent_mod)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
mods_to_disable.add(mod)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
mods_to_disable.to_a
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Show the disable plan to user
|
|
155
|
+
#
|
|
156
|
+
# @param mods_to_disable [Array<Factorix::MOD>] MODs to disable
|
|
157
|
+
# @return [void]
|
|
158
|
+
private def show_plan(mods_to_disable)
|
|
159
|
+
if mods_to_disable.empty?
|
|
160
|
+
say "All specified MOD(s) are already disabled", prefix: :info
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
say "Planning to disable #{mods_to_disable.size} MOD(s):", prefix: :info
|
|
165
|
+
mods_to_disable.each do |mod|
|
|
166
|
+
say " - #{mod}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Execute the disable plan
|
|
171
|
+
#
|
|
172
|
+
# @param mods_to_disable [Array<Factorix::MOD>] MODs to disable
|
|
173
|
+
# @param mod_list [Factorix::MODList] MOD list to modify
|
|
174
|
+
# @return [void]
|
|
175
|
+
private def execute_plan(mods_to_disable, mod_list)
|
|
176
|
+
return if mods_to_disable.empty?
|
|
177
|
+
|
|
178
|
+
mods_to_disable.each do |mod|
|
|
179
|
+
mod_list.disable(mod)
|
|
180
|
+
say "Disabled #{mod}", prefix: :success
|
|
181
|
+
logger.debug("Disabled MOD", mod_name: mod.name)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|