mnenv 0.1.1 → 0.1.2

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: '090048897f563d0162ceda95dd4f727cda725916b40e7150d815280dc9592ef6'
4
- data.tar.gz: 0fbf4dfbbff09224c9672fef91b5607289848166bb58eaeb904d6633eecfcfdf
3
+ metadata.gz: 9bb839d75925723bae3de1d40012914fc768b8f981d4be837247ea0b8de8bf2d
4
+ data.tar.gz: 1335ac68fba15671370c1c42f4d6999707915fd3ec884d33ee47759f3ba95863
5
5
  SHA512:
6
- metadata.gz: 4695512492c103578d75100163c8201407473f73d53c67e1ebf684e7b6f248d29012caf1f17b504e5f1b4167de34199f07b5c04b09e6749fcc0634c7bcc53a8c
7
- data.tar.gz: cea3fa81d4e70f8516d5866b5ee144ae8e734deccc09d80a3269d1deb98207f8af9468c82c89ef2e68256f9336d8e607968c9b59116d9f5644ca1c5cdf962c40
6
+ metadata.gz: b09af8fc57ec321e46c1b4976ff69e73f00b9443168659b7e343cb8a1f527ef64eaa0ea9dc0265fda6e4d8b678d351bca6fe1adca4b35ded707ddc1e4b895b18
7
+ data.tar.gz: 68a851a68c5a2feafe61cb47b0e68f98dc328d78c7e5459b31440250048c03fd19724cf2fcdf2462e46c6b00c6fdf6e33514d1c24569929d539fdaa8fe7efb69
@@ -7,36 +7,105 @@ module Mnenv
7
7
  class UninstallCommand < Thor
8
8
  namespace :uninstall
9
9
 
10
+ class_option :source, type: :string, enum: %w[gemfile binary],
11
+ desc: 'Source type to uninstall (gemfile or binary). If not specified, uninstalls all sources.'
10
12
  class_option :force, type: :boolean, aliases: '-f', default: false,
11
13
  desc: 'Force uninstallation without confirmation'
12
14
 
13
15
  desc 'VERSION', 'Uninstall a specific Metanorma version'
16
+ method_option :source, type: :string, enum: %w[gemfile binary]
14
17
  method_option :force, type: :boolean, aliases: '-f', default: false
15
18
  def uninstall(version)
16
- version_dir = File.expand_path("~/.mnenv/versions/#{version}")
19
+ source = options[:source]
20
+
21
+ if source
22
+ # Uninstall specific source
23
+ uninstall_source(version, source)
24
+ else
25
+ # Uninstall all sources for this version
26
+ uninstall_all_sources(version)
27
+ end
28
+
29
+ # Regenerate shims after uninstallation
30
+ ShimManager.new.regenerate_all
31
+ rescue StandardError => e
32
+ warn "Error: #{e.message}"
33
+ exit 1
34
+ end
35
+
36
+ private
37
+
38
+ def uninstall_source(version, source)
39
+ version_dir = Paths.version_install_dir(version, source)
17
40
 
18
41
  unless Dir.exist?(version_dir)
42
+ puts "Version #{version} (source: #{source}) is not installed."
43
+ return
44
+ end
45
+
46
+ confirm_and_remove(version, source, version_dir)
47
+ end
48
+
49
+ def uninstall_all_sources(version)
50
+ # Find all installed sources for this version
51
+ installed_sources = find_installed_sources(version)
52
+
53
+ if installed_sources.empty?
19
54
  puts "Version #{version} is not installed."
20
55
  return
21
56
  end
22
57
 
23
- unless options[:force]
58
+ if installed_sources.length == 1
59
+ # Only one source, uninstall directly
60
+ source = installed_sources.first
61
+ version_dir = Paths.version_install_dir(version, source)
62
+ confirm_and_remove(version, source, version_dir)
63
+ else
64
+ # Multiple sources, ask which to uninstall
65
+ puts "Version #{version} has multiple sources installed:"
66
+ installed_sources.each do |src|
67
+ puts " - #{src}"
68
+ end
69
+ puts ''
70
+
71
+ prompt = TTY::Prompt.new
72
+ choices = installed_sources.map { |s| { name: s, value: s } }
73
+ choices << { name: 'All sources', value: 'all' }
74
+
75
+ selected = prompt.select('Which source(s) to uninstall?', choices)
76
+
77
+ if selected == 'all'
78
+ installed_sources.each do |src|
79
+ version_dir = Paths.version_install_dir(version, src)
80
+ confirm_and_remove(version, src, version_dir, skip_confirm: options[:force])
81
+ end
82
+ else
83
+ version_dir = Paths.version_install_dir(version, selected)
84
+ confirm_and_remove(version, selected, version_dir)
85
+ end
86
+ end
87
+ end
88
+
89
+ def find_installed_sources(version)
90
+ sources = []
91
+ %w[gemfile binary].each do |source|
92
+ version_dir = Paths.version_install_dir(version, source)
93
+ sources << source if Dir.exist?(version_dir)
94
+ end
95
+ sources
96
+ end
97
+
98
+ def confirm_and_remove(version, source, version_dir, skip_confirm: false)
99
+ unless skip_confirm || options[:force]
24
100
  prompt = TTY::Prompt.new
25
- unless prompt.yes?("Uninstall Metanorma #{version}? This cannot be undone.")
101
+ unless prompt.yes?("Uninstall Metanorma #{version} (#{source})? This cannot be undone.")
26
102
  puts 'Uninstallation cancelled.'
27
103
  return
28
104
  end
29
105
  end
30
106
 
31
107
  FileUtils.rm_rf(version_dir)
32
-
33
- # Regenerate shims
34
- ShimManager.new.regenerate_all
35
-
36
- puts "Uninstalled Metanorma #{version}"
37
- rescue StandardError => e
38
- warn "Error: #{e.message}"
39
- exit 1
108
+ puts "Uninstalled Metanorma #{version} (source: #{source})"
40
109
  end
41
110
  end
42
111
  end
@@ -3,7 +3,7 @@
3
3
  require 'tty/prompt'
4
4
  require 'json'
5
5
  require_relative '../installer'
6
- require_relative '../binary_repository'
6
+ require_relative '../version_resolver'
7
7
 
8
8
  module Mnenv
9
9
  class VersionCommand < Thor
@@ -68,7 +68,7 @@ module Mnenv
68
68
  return
69
69
  end
70
70
 
71
- current_version, current_source = resolve_current
71
+ current_version, current_source = resolver.resolve
72
72
 
73
73
  case options[:format]
74
74
  when 'json'
@@ -105,6 +105,11 @@ module Mnenv
105
105
 
106
106
  private
107
107
 
108
+ # Get the shared resolver instance
109
+ def resolver
110
+ @resolver ||= VersionResolver.new
111
+ end
112
+
108
113
  # List installed versions with their sources
109
114
  # Uses Paths::INSTALLED_DIR with new naming convention: <version>-<source>
110
115
  def list_installed_versions
@@ -129,7 +134,7 @@ module Mnenv
129
134
 
130
135
  def resolve_version_and_source(version, source, interactive)
131
136
  version, source = select_version_interactive if interactive || version.nil?
132
- source ||= default_source
137
+ source ||= resolver.resolve_source
133
138
  [version, source]
134
139
  end
135
140
 
@@ -140,14 +145,12 @@ module Mnenv
140
145
  raise 'No versions installed. Run: mnenv install --list' if installed.empty?
141
146
 
142
147
  choices = installed.flat_map do |version, sources|
143
- sources.map do |source|
144
- { name: "#{version} (#{source})", value: [version, source] }
148
+ sources.map do |src|
149
+ { name: "#{version} (#{src})", value: [version, src] }
145
150
  end
146
151
  end
147
152
 
148
- version, source = prompt.select('Select a version:', choices)
149
-
150
- [version, source]
153
+ prompt.select('Select a version:', choices)
151
154
  end
152
155
 
153
156
  def verify_installed!(version, source)
@@ -160,54 +163,5 @@ module Mnenv
160
163
  def installed_versions
161
164
  list_installed_versions.keys.sort
162
165
  end
163
-
164
- def default_source
165
- if File.exist?(Paths::SOURCE_FILE)
166
- File.read(Paths::SOURCE_FILE).strip
167
- else
168
- 'gemfile'
169
- end
170
- end
171
-
172
- def resolve_version
173
- return ENV['METANORMA_VERSION'] if ENV['METANORMA_VERSION']
174
-
175
- dir = Dir.pwd
176
- loop do
177
- return File.read(File.join(dir, '.metanorma-version')).strip if File.exist?(File.join(dir,
178
- '.metanorma-version'))
179
-
180
- parent = File.dirname(dir)
181
- break if parent == dir # Reached root
182
-
183
- dir = parent
184
- end
185
-
186
- return File.read(Paths::VERSION_FILE).strip if File.exist?(Paths::VERSION_FILE)
187
-
188
- nil
189
- end
190
-
191
- def resolve_source
192
- return ENV['METANORMA_SOURCE'] if ENV['METANORMA_SOURCE']
193
-
194
- dir = Dir.pwd
195
- loop do
196
- return File.read(File.join(dir, '.metanorma-source')).strip if File.exist?(File.join(dir, '.metanorma-source'))
197
-
198
- parent = File.dirname(dir)
199
- break if parent == dir # Reached root
200
-
201
- dir = parent
202
- end
203
-
204
- return File.read(Paths::SOURCE_FILE).strip if File.exist?(Paths::SOURCE_FILE)
205
-
206
- 'gemfile'
207
- end
208
-
209
- def resolve_current
210
- [resolve_version, resolve_source]
211
- end
212
166
  end
213
167
  end
@@ -1,17 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../source_registry'
4
+
3
5
  module Mnenv
6
+ # Factory for creating installer instances based on source type
7
+ # Uses SourceRegistry for extensibility - new sources just need to register
4
8
  class InstallerFactory
5
- def self.create(version, source:)
6
- case source.to_s
7
- when 'gemfile'
8
- Installers::GemfileInstaller.new(version, source: source)
9
- when 'binary'
10
- Installers::BinaryInstaller.new(version, source: source)
11
- else
9
+ class UnknownSourceError < StandardError; end
10
+
11
+ # Create an installer for the given version and source
12
+ # @param version [String] The version to install
13
+ # @param source [String] The source type (e.g., 'gemfile', 'binary')
14
+ # @param target_dir [String, nil] Optional custom target directory
15
+ # @return [Installer] An installer instance
16
+ # @raise [UnknownSourceError] If source is not registered
17
+ def self.create(version, source:, target_dir: nil)
18
+ installer_class = SourceRegistry.installer(source.to_s)
19
+
20
+ unless installer_class
21
+ available = SourceRegistry.all_names.join(', ')
12
22
  raise Installer::InstallationError,
13
- "Unknown source: #{source}. Use: gemfile or binary"
23
+ "Unknown source: #{source}. Available sources: #{available}"
14
24
  end
25
+
26
+ installer_class.new(version, source: source, target_dir: target_dir)
27
+ rescue SourceRegistry::UnknownSourceError
28
+ available = SourceRegistry.all_names.join(', ')
29
+ raise Installer::InstallationError,
30
+ "Unknown source: #{source}. Available sources: #{available}"
31
+ end
32
+
33
+ # Check if a source is supported
34
+ # @param source [String] The source type
35
+ # @return [Boolean]
36
+ def self.supported?(source)
37
+ SourceRegistry.registered?(source.to_s)
38
+ end
39
+
40
+ # Get list of supported sources
41
+ # @return [Array<String>]
42
+ def self.supported_sources
43
+ SourceRegistry.all_names
15
44
  end
16
45
  end
17
46
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnenv
4
+ # Centralized platform detection for consistent OS/architecture/variant detection
5
+ # across the codebase. Used by binary installer, repository, and CLI.
6
+ class PlatformDetector
7
+ class UnsupportedPlatform < StandardError; end
8
+
9
+ class << self
10
+ # Detect the current operating system
11
+ # @return [String] 'linux', 'darwin', or 'windows'
12
+ # @raise [UnsupportedPlatform] if platform is not supported
13
+ def os
14
+ case RbConfig::CONFIG['host_os']
15
+ when /linux/
16
+ 'linux'
17
+ when /darwin/
18
+ 'darwin'
19
+ when /mswin|mingw|cygwin/
20
+ 'windows'
21
+ else
22
+ raise UnsupportedPlatform, "Unsupported platform: #{RbConfig::CONFIG['host_os']}"
23
+ end
24
+ end
25
+
26
+ # Detect the current CPU architecture
27
+ # @return [String] 'arm64' or 'x86_64'
28
+ def arch
29
+ case RbConfig::CONFIG['host_cpu']
30
+ when /arm64|aarch64/
31
+ 'arm64'
32
+ when /x86_64|x64/
33
+ 'x86_64'
34
+ else
35
+ 'x86_64' # Default fallback
36
+ end
37
+ end
38
+
39
+ # Detect libc variant (for Linux)
40
+ # @return [String, nil] 'musl' for Alpine/musl systems, nil for glibc
41
+ def variant
42
+ return nil unless os == 'linux'
43
+
44
+ 'musl' if musl?
45
+ end
46
+
47
+ # Check if running on a musl-based system (Alpine Linux)
48
+ # @return [Boolean]
49
+ def musl?
50
+ File.exist?('/etc/alpine-release') ||
51
+ File.symlink?('/lib/libc.musl-x86_64.so.1')
52
+ rescue StandardError
53
+ false
54
+ end
55
+
56
+ # Check if running on Windows
57
+ # @return [Boolean]
58
+ def windows?
59
+ os == 'windows'
60
+ end
61
+
62
+ # Check if running on macOS
63
+ # @return [Boolean]
64
+ def macos?
65
+ os == 'darwin'
66
+ end
67
+
68
+ # Check if running on Linux
69
+ # @return [Boolean]
70
+ def linux?
71
+ os == 'linux'
72
+ end
73
+
74
+ # Get platform info as a hash
75
+ # @return [Hash] With keys :os, :arch, :variant
76
+ def to_h
77
+ {
78
+ os: os,
79
+ arch: arch,
80
+ variant: variant
81
+ }
82
+ end
83
+
84
+ # Get a human-readable platform string
85
+ # @return [String] e.g., "linux-x86_64", "darwin-arm64", "linux-musl-x86_64"
86
+ def platform_string
87
+ parts = [os]
88
+ parts << variant if variant
89
+ parts << arch
90
+ parts.join('-')
91
+ end
92
+
93
+ # Get all possible platform strings for binary matching
94
+ # (includes variant-specific and generic versions)
95
+ # @return [Array<String>] List of platform strings in order of preference
96
+ def platform_candidates
97
+ candidates = []
98
+
99
+ # With variant (if applicable)
100
+ candidates << "#{os}-#{variant}-#{arch}" if variant
101
+
102
+ # Without variant
103
+ candidates << "#{os}-#{arch}"
104
+
105
+ candidates
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'source_registry'
4
+ require_relative 'gemfile_repository'
5
+ require_relative 'binary_repository'
6
+ require_relative 'models/gemfile_version'
7
+ require_relative 'models/binary_version'
8
+ require_relative 'installers/gemfile_installer'
9
+ require_relative 'installers/binary_installer'
10
+
11
+ module Mnenv
12
+ # Auto-registration of built-in sources
13
+ # This enables the plugin architecture - new sources just need to register themselves
14
+ module Sources
15
+ class << self
16
+ # Register all built-in sources
17
+ def setup
18
+ register_gemfile
19
+ register_binary
20
+ end
21
+
22
+ private
23
+
24
+ def register_gemfile
25
+ SourceRegistry.register(
26
+ name: 'gemfile',
27
+ repository: GemfileRepository,
28
+ model: GemfileVersion,
29
+ installer: Installers::GemfileInstaller
30
+ )
31
+ end
32
+
33
+ def register_binary
34
+ SourceRegistry.register(
35
+ name: 'binary',
36
+ repository: BinaryRepository,
37
+ model: BinaryVersion,
38
+ installer: Installers::BinaryInstaller
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # Auto-setup on load
46
+ Mnenv::Sources.setup
data/lib/mnenv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mnenv
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
5
5
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnenv
4
+ # Centralized version and source resolution with clear precedence:
5
+ # 1. Environment variables (METANORMA_VERSION, METANORMA_SOURCE)
6
+ # 2. Local files (.metanorma-version, .metanorma-source) - walk up tree
7
+ # 3. Global files (~/.mnenv/version, ~/.mnenv/source)
8
+ # 4. Defaults (gemfile for source)
9
+ #
10
+ # This class is the single source of truth for version resolution.
11
+ # The Bash resolver (lib/mnenv/resolver) is kept for shims only.
12
+ class VersionResolver
13
+ # Resolve the current Metanorma version
14
+ # @return [String, nil] The resolved version or nil if not set
15
+ def resolve_version
16
+ from_env('METANORMA_VERSION') ||
17
+ from_local_file('.metanorma-version') ||
18
+ from_global_file(Paths::VERSION_FILE)
19
+ end
20
+
21
+ # Resolve the current Metanorma source
22
+ # @return [String] The resolved source (defaults to 'gemfile')
23
+ def resolve_source
24
+ from_env('METANORMA_SOURCE') ||
25
+ from_local_file('.metanorma-source') ||
26
+ from_global_file(Paths::SOURCE_FILE) ||
27
+ 'gemfile'
28
+ end
29
+
30
+ # Resolve both version and source
31
+ # @return [Array<String, String>] Tuple of [version, source]
32
+ def resolve
33
+ [resolve_version, resolve_source]
34
+ end
35
+
36
+ # Check if a version is set anywhere
37
+ # @return [Boolean]
38
+ def version_set?
39
+ !resolve_version.nil?
40
+ end
41
+
42
+ # Check if a source is explicitly set (not default)
43
+ # @return [Boolean]
44
+ def source_set?
45
+ from_env('METANORMA_SOURCE') ||
46
+ from_local_file('.metanorma-source') ||
47
+ from_global_file(Paths::SOURCE_FILE)
48
+ end
49
+
50
+ # Get the source of version resolution (for debugging)
51
+ # @return [Symbol] :environment, :local, :global, or :none
52
+ def version_source
53
+ return :environment if from_env('METANORMA_VERSION')
54
+ return :local if from_local_file('.metanorma-version')
55
+ return :global if from_global_file(Paths::VERSION_FILE)
56
+
57
+ :none
58
+ end
59
+
60
+ # Get the source of source resolution (for debugging)
61
+ # @return [Symbol] :environment, :local, :global, or :default
62
+ def source_source
63
+ return :environment if from_env('METANORMA_SOURCE')
64
+ return :local if from_local_file('.metanorma-source')
65
+ return :global if from_global_file(Paths::SOURCE_FILE)
66
+
67
+ :default
68
+ end
69
+
70
+ private
71
+
72
+ # Read from environment variable
73
+ # @param var [String] Environment variable name
74
+ # @return [String, nil] Value or nil if not set/empty
75
+ def from_env(var)
76
+ value = ENV[var]
77
+ value&.strip&.then { |v| v unless v.empty? }
78
+ end
79
+
80
+ # Read from local file, walking up directory tree
81
+ # @param filename [String] Filename to look for
82
+ # @return [String, nil] File contents or nil if not found
83
+ def from_local_file(filename)
84
+ dir = Dir.pwd
85
+
86
+ loop do
87
+ path = File.join(dir, filename)
88
+ return File.read(path).strip if File.exist?(path)
89
+
90
+ parent = File.dirname(dir)
91
+ break if parent == dir # Reached root
92
+
93
+ dir = parent
94
+ end
95
+
96
+ nil
97
+ end
98
+
99
+ # Read from global file
100
+ # @param path [String] Full path to file
101
+ # @return [String, nil] File contents or nil if not found
102
+ def from_global_file(path)
103
+ File.read(path).strip if File.exist?(path)
104
+ rescue StandardError
105
+ nil
106
+ end
107
+ end
108
+ end
data/lib/mnenv.rb CHANGED
@@ -4,8 +4,11 @@ require 'lutaml/model'
4
4
  require 'thor'
5
5
 
6
6
  require_relative 'mnenv/paths'
7
+ require_relative 'mnenv/platform_detector'
8
+ require_relative 'mnenv/version_resolver'
7
9
  require_relative 'mnenv/models'
10
+ require_relative 'mnenv/source_registry'
11
+ require_relative 'mnenv/sources'
8
12
  require_relative 'mnenv/commands'
9
13
  require_relative 'mnenv/installer'
10
- require_relative 'mnenv/source_registry'
11
14
  require_relative 'mnenv/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mnenv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -209,6 +209,7 @@ files:
209
209
  - lib/mnenv/models/snap_version.rb
210
210
  - lib/mnenv/models/version.rb
211
211
  - lib/mnenv/paths.rb
212
+ - lib/mnenv/platform_detector.rb
212
213
  - lib/mnenv/repository.rb
213
214
  - lib/mnenv/resolver
214
215
  - lib/mnenv/shells/base.rb
@@ -221,7 +222,9 @@ files:
221
222
  - lib/mnenv/snap/fetcher.rb
222
223
  - lib/mnenv/snap_repository.rb
223
224
  - lib/mnenv/source_registry.rb
225
+ - lib/mnenv/sources.rb
224
226
  - lib/mnenv/version.rb
227
+ - lib/mnenv/version_resolver.rb
225
228
  - lib/mnenv/versions_manager.rb
226
229
  - mnenv.gemspec
227
230
  - scripts/cross-source-switching-test.sh