vagrant-orbstack 0.1.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.
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'orbstack_cli'
5
+ require_relative '../errors'
6
+
7
+ module VagrantPlugins
8
+ module OrbStack
9
+ module Util
10
+ # Utility class for generating unique machine names with collision avoidance
11
+ #
12
+ # Generates machine names in the format: vagrant-<sanitized-name>-<short-id>
13
+ # where short-id is a 6-character random hex string. Implements collision
14
+ # detection and automatic retry with new IDs.
15
+ #
16
+ # @example Generate a unique machine name
17
+ # machine = double('machine', name: 'web_server')
18
+ # name = MachineNamer.generate(machine)
19
+ # # => "vagrant-web-server-a3b2c1"
20
+ #
21
+ # @example Collision handling
22
+ # # If "vagrant-default-a3b2c1" exists, generates new ID automatically
23
+ # name = MachineNamer.generate(machine)
24
+ # # => "vagrant-default-d4e5f6" (different ID)
25
+ #
26
+ # @api public
27
+ class MachineNamer
28
+ # Maximum number of retry attempts for collision avoidance
29
+ MAX_RETRIES = 3
30
+
31
+ # Maximum machine name length (DNS hostname limit)
32
+ MAX_NAME_LENGTH = 63
33
+
34
+ # Generate a unique machine name with collision avoidance.
35
+ #
36
+ # Creates a machine name in the format vagrant-<name>-<id> where:
37
+ # - name is sanitized from machine.name (lowercase, hyphens, alphanumeric)
38
+ # - id is a 6-character random hex string
39
+ #
40
+ # If a collision is detected (name already exists in OrbStack), retries
41
+ # with a new random ID up to MAX_RETRIES times.
42
+ #
43
+ # @param machine [Vagrant::Machine] The machine object with name attribute
44
+ # @return [String] A unique machine name
45
+ # @raise [MachineNameCollisionError] If all retry attempts are exhausted
46
+ # @raise [OrbStackNotInstalled] If OrbStack CLI is not available
47
+ # @raise [CommandTimeoutError] If OrbStack CLI times out
48
+ # @api public
49
+ def self.generate(machine)
50
+ machine_name = machine.name.to_s
51
+ sanitized = sanitize_name(machine_name)
52
+
53
+ MAX_RETRIES.times do
54
+ short_id = SecureRandom.hex(3)
55
+ candidate = "vagrant-#{sanitized}-#{short_id}"
56
+
57
+ return candidate unless check_collision?(candidate)
58
+ end
59
+
60
+ # All retries exhausted - raise error
61
+ raise MachineNameCollisionError,
62
+ "Failed to generate unique machine name after #{MAX_RETRIES} attempts (machine: #{machine_name})"
63
+ end
64
+
65
+ # Sanitize a machine name for use in hostname.
66
+ #
67
+ # Applies the following transformations:
68
+ # - Convert to lowercase
69
+ # - Replace underscores with hyphens
70
+ # - Remove all characters except alphanumeric and hyphens
71
+ # - Strip leading/trailing whitespace
72
+ # - Collapse consecutive hyphens to single hyphen
73
+ # - Truncate to fit within MAX_NAME_LENGTH minus prefix/suffix space
74
+ # - Default to "default" if result is empty
75
+ #
76
+ # @param name [String, nil] The name to sanitize
77
+ # @return [String] The sanitized name
78
+ # @api private
79
+ class << self
80
+ private
81
+
82
+ # Method length acceptable: Each transformation step is clear and well-documented.
83
+ # Combining steps would reduce readability.
84
+ # rubocop:disable Metrics/MethodLength
85
+ def sanitize_name(name)
86
+ # Handle nil or empty input → default
87
+ return 'default' if name.nil? || name.strip.empty?
88
+
89
+ # Apply sanitization rules:
90
+ # 1. Strip whitespace
91
+ # 2. Convert to lowercase
92
+ # 3. Replace underscores with hyphens (DNS-safe)
93
+ # 4. Remove non-alphanumeric (except hyphens)
94
+ # 5. Collapse consecutive hyphens
95
+ # 6. Remove leading/trailing hyphens
96
+ sanitized = name.to_s
97
+ .strip # Rule 1
98
+ .downcase # Rule 2
99
+ .gsub('_', '-') # Rule 3
100
+ .gsub(/[^a-z0-9-]/, '') # Rule 4
101
+ .gsub(/-+/, '-') # Rule 5
102
+ .gsub(/^-|-$/, '') # Rule 6
103
+
104
+ return 'default' if sanitized.empty?
105
+
106
+ # DNS limit (63) - "vagrant-" (8) - "-XXXXXX" (7) = 48 max
107
+ max_length = MAX_NAME_LENGTH - 15
108
+ sanitized[0...max_length]
109
+ end
110
+ # rubocop:enable Metrics/MethodLength
111
+
112
+ # Check if a machine name already exists in OrbStack.
113
+ #
114
+ # Queries OrbStack CLI for list of existing machines and checks
115
+ # if the candidate name is already in use.
116
+ #
117
+ # @param name [String] The candidate name to check
118
+ # @return [Boolean] true if collision detected, false otherwise
119
+ # @raise [OrbStackNotInstalled] If OrbStack CLI is not available
120
+ # @raise [CommandTimeoutError] If OrbStack CLI times out
121
+ # @api private
122
+ def check_collision?(name)
123
+ machines = OrbStackCLI.list_machines
124
+ machines.any? { |m| m[:name] == name }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'timeout'
5
+ require 'json'
6
+ require 'vagrant-orbstack/errors'
7
+
8
+ module VagrantPlugins
9
+ module OrbStack
10
+ module Util
11
+ # Utility class for detecting and interacting with OrbStack CLI
12
+ #
13
+ # This class provides a Ruby interface to the OrbStack command-line tool (`orb`).
14
+ # All methods execute shell commands and return parsed results. The class handles
15
+ # timeouts, error detection, and logging automatically.
16
+ #
17
+ # @example Check if OrbStack is available
18
+ # if OrbStackCLI.available?
19
+ # puts "OrbStack version: #{OrbStackCLI.version}"
20
+ # end
21
+ #
22
+ # @example Create and manage a machine
23
+ # OrbStackCLI.create_machine('ubuntu', 'my-dev-vm')
24
+ # OrbStackCLI.start_machine('my-dev-vm')
25
+ # info = OrbStackCLI.machine_info('my-dev-vm')
26
+ # OrbStackCLI.stop_machine('my-dev-vm')
27
+ #
28
+ # @example List all machines
29
+ # machines = OrbStackCLI.list_machines
30
+ # machines.each do |m|
31
+ # puts "#{m[:name]} - #{m[:status]}"
32
+ # end
33
+ class OrbStackCLI
34
+ # Default timeout for non-mutating query operations (list, info).
35
+ # Use this for fast read operations.
36
+ # @return [Integer] Timeout in seconds
37
+ QUERY_TIMEOUT = 30
38
+
39
+ # Default timeout for state-changing operations (start, stop, delete).
40
+ # Use this for operations that modify machine state.
41
+ # @return [Integer] Timeout in seconds
42
+ MUTATE_TIMEOUT = 60
43
+
44
+ # Default timeout for machine creation operations.
45
+ # Longer timeout accounts for distribution image downloads which
46
+ # can take 30-120 seconds on first use.
47
+ # @return [Integer] Timeout in seconds
48
+ CREATE_TIMEOUT = 120
49
+
50
+ @logger = Log4r::Logger.new('vagrant_orbstack::util')
51
+
52
+ class << self
53
+ # Check if orb command is available in PATH
54
+ # @return [Boolean] true if orb command exists, false otherwise
55
+ def available?
56
+ stdout, _stderr, success = execute_command('which orb')
57
+ !stdout.empty? && success
58
+ end
59
+
60
+ # Get OrbStack version
61
+ # @return [String, nil] version string or nil if not available
62
+ def version
63
+ stdout, _stderr, success = execute_command('orb --version')
64
+ return nil unless success
65
+
66
+ # Parse version from output like "orb version 1.2.3" or just "1.2.3"
67
+ match = stdout.match(/(\d+\.\d+\.\d+)/)
68
+ match ? match[1] : nil
69
+ end
70
+
71
+ # Check if OrbStack is currently running
72
+ # @return [Boolean] true if running, false otherwise
73
+ def running?
74
+ stdout, _stderr, success = execute_command('orb status')
75
+ success && stdout.match?(/running/i)
76
+ end
77
+
78
+ # List all OrbStack machines
79
+ #
80
+ # Executes `orb list` and parses the output into an array of machine hashes.
81
+ # Each hash contains the machine name and current status.
82
+ #
83
+ # @return [Array<Hash>] Array of machine hashes, each with:
84
+ # - :name [String] The machine name
85
+ # - :status [String] The machine status (e.g., 'running', 'stopped')
86
+ # @return [Array] Empty array on failure or if no machines exist
87
+ # @raise [CommandTimeoutError] If command times out
88
+ #
89
+ # @example List all machines
90
+ # machines = OrbStackCLI.list_machines
91
+ # # => [{name: 'ubuntu-dev', status: 'running'}, {name: 'debian-test', status: 'stopped'}]
92
+ # rubocop:disable Metrics/MethodLength
93
+ def list_machines
94
+ stdout, _stderr, success = execute_command('orb list', timeout: QUERY_TIMEOUT)
95
+ return [] unless success
96
+ return [] if stdout.empty?
97
+
98
+ # Parse machine list output (format: NAME STATUS DISTRO IP)
99
+ machines = []
100
+ stdout.each_line do |line|
101
+ parts = line.strip.split(/\s+/)
102
+ next if parts.empty?
103
+
104
+ machines << {
105
+ name: parts[0],
106
+ status: parts[1]
107
+ }
108
+ end
109
+
110
+ machines
111
+ end
112
+ # rubocop:enable Metrics/MethodLength
113
+
114
+ # Get detailed information about a specific machine
115
+ #
116
+ # Executes `orb info <name>` and parses the JSON output. Returns nil if
117
+ # the machine doesn't exist or if JSON parsing fails.
118
+ #
119
+ # @param name [String] The machine name
120
+ # @return [Hash, nil] Parsed machine information hash with OrbStack-specific
121
+ # fields, or nil if machine not found or parsing fails
122
+ # @raise [CommandTimeoutError] If command times out
123
+ #
124
+ # @example Get machine info
125
+ # info = OrbStackCLI.machine_info('my-dev-vm')
126
+ # puts info['distro'] if info
127
+ def machine_info(name)
128
+ stdout, _stderr, success = execute_command("orb info --format json #{name}", timeout: QUERY_TIMEOUT)
129
+ return nil unless success
130
+
131
+ JSON.parse(stdout)
132
+ rescue JSON::ParserError => e
133
+ @logger.warn("Failed to parse machine info JSON: #{e.message}")
134
+ nil
135
+ end
136
+
137
+ # Create a new OrbStack machine
138
+ #
139
+ # Executes `orb create <distro> <name>`. This operation can take 30-120 seconds
140
+ # on first use if the distribution image needs to be downloaded. Subsequent
141
+ # creations of the same distribution are much faster.
142
+ #
143
+ # @param name [String] The machine name
144
+ # @param distribution [String] The distribution to use (e.g., 'ubuntu:noble', 'debian')
145
+ # @param timeout [Integer] Command timeout in seconds (default: CREATE_TIMEOUT)
146
+ # @return [Hash] Machine info hash with :id and :status keys
147
+ # @raise [CommandExecutionError] If creation fails
148
+ # @raise [CommandTimeoutError] If command times out
149
+ #
150
+ # @example Create an Ubuntu machine
151
+ # OrbStackCLI.create_machine('my-dev-vm', distribution: 'ubuntu:noble')
152
+ #
153
+ # @example Create with custom timeout for slow networks
154
+ # OrbStackCLI.create_machine('my-vm', distribution: 'ubuntu', timeout: 180)
155
+ # rubocop:disable Naming/PredicateMethod
156
+ def create_machine(name, distribution:, timeout: CREATE_TIMEOUT)
157
+ _, stderr, success = execute_command("orb create #{distribution} #{name}", timeout: timeout)
158
+ raise_unless_successful!('create', stderr, success)
159
+ { id: name, status: 'running' }
160
+ end
161
+
162
+ # Delete an OrbStack machine
163
+ #
164
+ # Executes `orb delete <name>`. This permanently removes the machine and
165
+ # all its data. The operation cannot be undone.
166
+ #
167
+ # @param name [String] The machine name
168
+ # @return [Boolean] true on success
169
+ # @raise [CommandExecutionError] If deletion fails
170
+ # @raise [CommandTimeoutError] If command times out
171
+ #
172
+ # @example Delete a machine
173
+ # OrbStackCLI.delete_machine('my-dev-vm')
174
+ def delete_machine(name)
175
+ _, stderr, success = execute_command("orb delete #{name}", timeout: MUTATE_TIMEOUT)
176
+ raise_unless_successful!('delete', stderr, success)
177
+ true
178
+ end
179
+
180
+ # Start an OrbStack machine
181
+ #
182
+ # Executes `orb start <name>`. Starts a stopped machine. If the machine
183
+ # is already running, this is a no-op.
184
+ #
185
+ # @param name [String] The machine name
186
+ # @param timeout [Integer] Command timeout in seconds (default: MUTATE_TIMEOUT)
187
+ # @return [Hash] Machine info hash with :id and :status keys
188
+ # @raise [CommandExecutionError] If start fails
189
+ # @raise [CommandTimeoutError] If command times out
190
+ #
191
+ # @example Start a machine
192
+ # OrbStackCLI.start_machine('my-dev-vm')
193
+ def start_machine(name, timeout: MUTATE_TIMEOUT)
194
+ _, stderr, success = execute_command("orb start #{name}", timeout: timeout)
195
+ raise_unless_successful!('start', stderr, success)
196
+ { id: name, status: 'running' }
197
+ end
198
+
199
+ # Stop an OrbStack machine
200
+ #
201
+ # Executes `orb stop <name>`. Stops a running machine gracefully. If the
202
+ # machine is already stopped, this is a no-op.
203
+ #
204
+ # @param name [String] The machine name
205
+ # @return [Boolean] true on success
206
+ # @raise [CommandExecutionError] If stop fails
207
+ # @raise [CommandTimeoutError] If command times out
208
+ #
209
+ # @example Stop a machine
210
+ # OrbStackCLI.stop_machine('my-dev-vm')
211
+ def stop_machine(name)
212
+ _, stderr, success = execute_command("orb stop #{name}", timeout: MUTATE_TIMEOUT)
213
+ raise_unless_successful!('stop', stderr, success)
214
+ true
215
+ end
216
+ # rubocop:enable Naming/PredicateMethod
217
+
218
+ private
219
+
220
+ # Raises CommandExecutionError unless the operation succeeded
221
+ #
222
+ # Helper method to enforce error handling for mutating operations.
223
+ # Only raises if the operation failed.
224
+ #
225
+ # @param action [String] The action being performed (e.g., 'create', 'delete')
226
+ # @param stderr [String] Error output from CLI
227
+ # @param success [Boolean] Whether the operation succeeded
228
+ # @return [void]
229
+ # @raise [CommandExecutionError] If success is false
230
+ def raise_unless_successful!(action, stderr, success)
231
+ return if success
232
+
233
+ raise CommandExecutionError, command: "orb #{action}", details: stderr
234
+ end
235
+
236
+ # Execute an OrbStack CLI command.
237
+ #
238
+ # Executes a shell command using Open3.capture3 and captures stdout,
239
+ # stderr, and exit status. Supports optional timeout for long-running
240
+ # operations. Logs command execution at DEBUG level on success and
241
+ # ERROR level on failure.
242
+ #
243
+ # On any StandardError (other than timeout), returns empty strings and
244
+ # false success flag rather than propagating the exception.
245
+ #
246
+ # @param command [String] The command to execute
247
+ # @param timeout [Integer] Timeout in seconds (default: QUERY_TIMEOUT)
248
+ # @return [Array<String, String, Boolean>] Tuple of [stdout, stderr, success].
249
+ # All string values are stripped of leading/trailing whitespace.
250
+ # @raise [CommandTimeoutError] If command exceeds timeout
251
+ # @api private
252
+ # rubocop:disable Metrics/MethodLength
253
+ def execute_command(command, timeout: QUERY_TIMEOUT)
254
+ stdout, stderr, status = if timeout
255
+ Timeout.timeout(timeout) do
256
+ Open3.capture3(command)
257
+ end
258
+ else
259
+ Open3.capture3(command)
260
+ end
261
+
262
+ success = status.success?
263
+
264
+ if success
265
+ @logger.debug("Command succeeded: #{command}")
266
+ else
267
+ @logger.error("Command failed: #{command}, stderr: #{stderr}")
268
+ end
269
+
270
+ [stdout.strip, stderr.strip, success]
271
+ rescue Timeout::Error
272
+ @logger.error("Command timed out after #{timeout}s: #{command}")
273
+ raise CommandTimeoutError, "Command timed out: #{command}"
274
+ rescue StandardError => e
275
+ @logger.error("Command error: #{e.message}")
276
+ ['', '', false]
277
+ end
278
+ # rubocop:enable Metrics/MethodLength
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'orbstack_cli'
4
+ require_relative '../errors'
5
+
6
+ module VagrantPlugins
7
+ module OrbStack
8
+ module Util
9
+ # Utility module for waiting until SSH becomes available on an OrbStack machine
10
+ #
11
+ # This module provides a polling mechanism to check if an OrbStack machine
12
+ # is ready for SSH connections. It repeatedly queries the machine status
13
+ # until the machine reports as 'running', indicating SSH is available.
14
+ #
15
+ # @example Wait for SSH on a newly created machine
16
+ # SSHReadinessChecker.wait_for_ready('my-machine', ui: ui)
17
+ #
18
+ # @example Handle timeout gracefully
19
+ # begin
20
+ # SSHReadinessChecker.wait_for_ready('my-machine', ui: ui)
21
+ # rescue VagrantPlugins::OrbStack::SSHNotReady => e
22
+ # ui.error("SSH not ready: #{e.message}")
23
+ # end
24
+ #
25
+ # @api public
26
+ module SSHReadinessChecker
27
+ # Maximum time to wait for SSH to become ready (in seconds)
28
+ # @return [Integer] Timeout in seconds
29
+ MAX_WAIT_TIME = 120
30
+
31
+ # Interval between status polls (in seconds)
32
+ # @return [Integer] Poll interval in seconds
33
+ POLL_INTERVAL = 2
34
+
35
+ # Wait for SSH to become available on a machine.
36
+ #
37
+ # Polls the machine status every POLL_INTERVAL seconds until the machine
38
+ # reports 'running' status, or until MAX_WAIT_TIME is exceeded. Displays
39
+ # progress messages via the provided UI object.
40
+ #
41
+ # @param machine_name [String] The name of the machine to check
42
+ # @param ui [Vagrant::UI::Interface] UI object for displaying progress messages
43
+ # @return [Boolean] true when SSH is ready
44
+ # @raise [SSHNotReady] If machine doesn't become ready within MAX_WAIT_TIME
45
+ # @raise [CommandExecutionError] If OrbStack CLI command fails
46
+ # @raise [CommandTimeoutError] If OrbStack CLI command times out
47
+ # @raise [OrbStackNotInstalled] If OrbStack CLI is not available
48
+ #
49
+ # @example Wait for SSH
50
+ # SSHReadinessChecker.wait_for_ready('ubuntu-dev', ui: machine.ui)
51
+ # # => true
52
+ #
53
+ # @api public
54
+ # rubocop:disable Naming/MethodParameterName
55
+ def self.wait_for_ready(machine_name, ui:)
56
+ # rubocop:enable Naming/MethodParameterName
57
+ ui.info("Waiting for SSH to become available on #{machine_name}...")
58
+
59
+ poll_until_ready(machine_name, ui) ||
60
+ raise_timeout_error(machine_name)
61
+ end
62
+
63
+ # Poll machine status until ready or timeout.
64
+ #
65
+ # @param machine_name [String] The name of the machine to check
66
+ # @param ui [Vagrant::UI::Interface] UI object for progress messages
67
+ # @return [Boolean, nil] true if ready, nil if timeout
68
+ # @api private
69
+ # rubocop:disable Naming/MethodParameterName, Metrics/MethodLength
70
+ def self.poll_until_ready(machine_name, ui)
71
+ # rubocop:enable Naming/MethodParameterName, Metrics/MethodLength
72
+ start_time = Time.now
73
+ elapsed = 0
74
+
75
+ while elapsed < MAX_WAIT_TIME
76
+ if machine_running?(machine_name)
77
+ ui.info('SSH is ready!')
78
+ return true
79
+ end
80
+
81
+ sleep(POLL_INTERVAL)
82
+ elapsed = Time.now - start_time
83
+ ui.info(" Still waiting... (#{elapsed.to_i} seconds elapsed)")
84
+ end
85
+
86
+ nil
87
+ end
88
+
89
+ # Raise timeout error with machine_name parameter for I18n.
90
+ #
91
+ # @param machine_name [String] The name of the machine
92
+ # @raise [SSHNotReady] Always raises with machine_name parameter
93
+ # @api private
94
+ def self.raise_timeout_error(machine_name)
95
+ raise SSHNotReady, machine_name: machine_name
96
+ end
97
+
98
+ # Check if machine is in running state.
99
+ #
100
+ # Queries OrbStack CLI for machine status and checks if it's 'running'.
101
+ # Handles nil responses and missing 'status' keys gracefully.
102
+ #
103
+ # @param machine_name [String] The name of the machine to check
104
+ # @return [Boolean] true if machine status is 'running', false otherwise
105
+ # @api private
106
+ def self.machine_running?(machine_name)
107
+ info = OrbStackCLI.machine_info(machine_name)
108
+ info && info.dig('record', 'state') == 'running'
109
+ end
110
+
111
+ private_class_method :poll_until_ready, :raise_timeout_error, :machine_running?
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module OrbStack
5
+ module Util
6
+ # TTL-based cache utility for machine state queries.
7
+ #
8
+ # This class provides a simple time-to-live (TTL) cache to reduce redundant
9
+ # CLI calls when querying machine state. State queries are cached with a
10
+ # configurable TTL (default 5 seconds), and automatically expire when the
11
+ # TTL is exceeded.
12
+ #
13
+ # The cache is designed for Vagrant's single-threaded environment and does
14
+ # not implement thread-safety mechanisms.
15
+ #
16
+ # @example Basic usage with default TTL
17
+ # cache = StateCache.new
18
+ # cache.set('machine-id', state)
19
+ # cached_state = cache.get('machine-id') # Returns state
20
+ #
21
+ # @example Custom TTL
22
+ # cache = StateCache.new(ttl: 10) # 10-second cache
23
+ # cache.set('key', 'value')
24
+ # # 11 seconds later...
25
+ # cache.get('key') # Returns nil (expired)
26
+ #
27
+ # @example Cache invalidation
28
+ # cache.invalidate('key') # Remove specific key
29
+ # cache.invalidate_all # Clear entire cache
30
+ class StateCache
31
+ # Default TTL for cached entries (in seconds)
32
+ DEFAULT_TTL = 5
33
+
34
+ # Initialize a new state cache.
35
+ #
36
+ # @param ttl [Integer] Time-to-live for cached entries in seconds (default: 5)
37
+ # @api public
38
+ def initialize(ttl: DEFAULT_TTL)
39
+ @ttl = ttl
40
+ @cache = {}
41
+ end
42
+
43
+ # Retrieve a cached value.
44
+ #
45
+ # Returns the cached value if it exists and has not expired. Returns nil
46
+ # if the key doesn't exist or if the cached entry has exceeded its TTL.
47
+ #
48
+ # @param key [String] The cache key
49
+ # @return [Object, nil] The cached value if valid, nil if missing or expired
50
+ # @api public
51
+ def get(key)
52
+ entry = @cache[key]
53
+ return nil unless entry
54
+
55
+ # Check if entry has expired
56
+ if Time.now - entry[:timestamp] > @ttl
57
+ # Entry expired, remove it and return nil
58
+ @cache.delete(key)
59
+ return nil
60
+ end
61
+
62
+ entry[:value]
63
+ end
64
+
65
+ # Store a value in the cache.
66
+ #
67
+ # Stores the value with the current timestamp. If the key already exists,
68
+ # its value and timestamp are overwritten.
69
+ #
70
+ # @param key [String] The cache key
71
+ # @param value [Object] The value to cache (can be any object, including nil)
72
+ # @return [void]
73
+ # @api public
74
+ def set(key, value)
75
+ @cache[key] = {
76
+ value: value,
77
+ timestamp: Time.now
78
+ }
79
+ end
80
+
81
+ # Invalidate a specific cache entry.
82
+ #
83
+ # Removes the specified key from the cache. This is a no-op if the key
84
+ # doesn't exist.
85
+ #
86
+ # @param key [String] The cache key to remove
87
+ # @return [void]
88
+ # @api public
89
+ def invalidate(key)
90
+ @cache.delete(key)
91
+ end
92
+
93
+ # Clear all cache entries.
94
+ #
95
+ # Removes all cached entries. This is useful when you need to ensure
96
+ # fresh data is retrieved on the next query.
97
+ #
98
+ # @return [void]
99
+ # @api public
100
+ def invalidate_all
101
+ @cache.clear
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module OrbStack
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'vagrant-orbstack/plugin'
5
+
6
+ module VagrantPlugins
7
+ # OrbStack provider for Vagrant.
8
+ #
9
+ # This module contains the implementation of the OrbStack provider plugin,
10
+ # enabling Vagrant to use OrbStack as a backend for managing Linux
11
+ # development environments on macOS.
12
+ #
13
+ # @see Plugin
14
+ # @see Provider
15
+ # @see Config
16
+ module OrbStack
17
+ lib_path = Pathname.new(File.join(__dir__, 'vagrant-orbstack'))
18
+ autoload :Action, lib_path.join('action')
19
+ autoload :Errors, lib_path.join('errors')
20
+
21
+ # Sentinel value to distinguish "not set" from nil.
22
+ # Used in configuration to differentiate between explicitly set to nil
23
+ # versus never configured (should use default).
24
+ #
25
+ # @api private
26
+ UNSET_VALUE = ::Object.new.freeze
27
+
28
+ # Returns the path to the source of this plugin.
29
+ #
30
+ # @return [Pathname] Root directory of the plugin source
31
+ # @api private
32
+ def self.source_root
33
+ @source_root ||= Pathname.new(File.join(__dir__, '..'))
34
+ end
35
+ end
36
+ end