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,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vagrant-orbstack'
4
+
5
+ module VagrantPlugins
6
+ module OrbStack
7
+ # Configuration class for OrbStack provider.
8
+ #
9
+ # This class defines configuration options available in Vagrantfiles
10
+ # for customizing OrbStack machine creation and behavior.
11
+ #
12
+ # @example Basic configuration
13
+ # Vagrant.configure("2") do |config|
14
+ # config.vm.provider :orbstack do |os|
15
+ # os.distro = "ubuntu"
16
+ # os.version = "22.04"
17
+ # os.machine_name = "my-dev-env"
18
+ # end
19
+ # end
20
+ #
21
+ # @api public
22
+ class Config < Vagrant.plugin('2', :config)
23
+ # @!attribute [rw] distro
24
+ # Linux distribution to use for the machine
25
+ # @return [String, nil] Distribution name (e.g., "ubuntu", "debian")
26
+ # @api public
27
+ #
28
+ # @!attribute [rw] version
29
+ # Distribution version to use
30
+ # @return [String, nil] Version string (e.g., "22.04")
31
+ # @api public
32
+ #
33
+ # @!attribute [rw] machine_name
34
+ # Custom name for the OrbStack machine
35
+ # @return [String, nil] Machine name
36
+ # @api public
37
+ #
38
+ # @!attribute [rw] ssh_username
39
+ # Custom SSH username for connecting to the machine
40
+ # @return [String, nil] SSH username (defaults to OrbStack's default if nil)
41
+ # @api public
42
+ #
43
+ # @!attribute [rw] forward_agent
44
+ # Enable SSH agent forwarding
45
+ # @return [Boolean, nil] Whether to forward SSH agent (defaults to false)
46
+ # @api public
47
+ attr_accessor :distro
48
+ attr_accessor :version, :machine_name, :ssh_username, :forward_agent
49
+
50
+ # Error message constants for validation
51
+ DISTRO_EMPTY_ERROR = 'distro cannot be empty'
52
+ MACHINE_NAME_FORMAT_ERROR = 'machine_name must contain only alphanumeric characters and hyphens'
53
+ SSH_USERNAME_EMPTY_ERROR = 'ssh_username cannot be empty'
54
+ FORWARD_AGENT_BOOLEAN_ERROR = 'forward_agent must be a boolean (true or false)'
55
+
56
+ # Regular expression pattern for valid machine_name format.
57
+ #
58
+ # Valid machine names must:
59
+ # - Start with an alphanumeric character (a-z, A-Z, 0-9)
60
+ # - End with an alphanumeric character
61
+ # - May contain hyphens between alphanumeric segments
62
+ # - No consecutive hyphens allowed
63
+ MACHINE_NAME_PATTERN = /^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/
64
+
65
+ # Initialize configuration with unset values.
66
+ #
67
+ # @api public
68
+ def initialize
69
+ super
70
+ @distro = VagrantPlugins::OrbStack::UNSET_VALUE
71
+ @version = VagrantPlugins::OrbStack::UNSET_VALUE
72
+ @machine_name = VagrantPlugins::OrbStack::UNSET_VALUE
73
+ @ssh_username = VagrantPlugins::OrbStack::UNSET_VALUE
74
+ @forward_agent = VagrantPlugins::OrbStack::UNSET_VALUE
75
+ @logger = Log4r::Logger.new('vagrant_orbstack::config')
76
+ end
77
+
78
+ # Finalize configuration by setting defaults.
79
+ #
80
+ # Called by Vagrant after Vagrantfile is loaded to set default values
81
+ # for any unset configuration options.
82
+ #
83
+ # @api public
84
+ def finalize!
85
+ @distro = 'ubuntu' if @distro == VagrantPlugins::OrbStack::UNSET_VALUE
86
+ @version = nil if @version == VagrantPlugins::OrbStack::UNSET_VALUE
87
+ @machine_name = nil if @machine_name == VagrantPlugins::OrbStack::UNSET_VALUE
88
+ @ssh_username = nil if @ssh_username == VagrantPlugins::OrbStack::UNSET_VALUE
89
+ @forward_agent = false if @forward_agent == VagrantPlugins::OrbStack::UNSET_VALUE
90
+ end
91
+
92
+ # Validate configuration values.
93
+ #
94
+ # @param [Vagrant::Machine] _machine The machine to validate for
95
+ # @return [Hash<String, Array<String>>] Validation errors by namespace
96
+ # @api public
97
+ def validate(_machine)
98
+ errors = _detected_errors
99
+ validate_distro(errors)
100
+ validate_machine_name(errors)
101
+ validate_ssh_username(errors)
102
+ validate_forward_agent(errors)
103
+ { 'OrbStack Provider' => errors }
104
+ end
105
+
106
+ private
107
+
108
+ # Validation methods use defensive type coercion (.to_s) to prevent
109
+ # NoMethodError crashes if attributes are accidentally set to non-String types.
110
+ # This approach prioritizes robustness over strict type enforcement.
111
+ #
112
+ # Pattern: Always check nil first, then coerce to string for validation.
113
+ #
114
+ # Example:
115
+ # return if @attribute.nil?
116
+ # errors << ERROR_CONSTANT if @attribute.to_s.strip.empty?
117
+
118
+ # Validate that distro attribute is not empty.
119
+ #
120
+ # @param errors [Array<String>] Error accumulator array
121
+ # @return [void]
122
+ def validate_distro(errors)
123
+ errors << DISTRO_EMPTY_ERROR if @distro.nil? || @distro.to_s.strip.empty?
124
+ end
125
+
126
+ # Validate machine_name format if set.
127
+ #
128
+ # @param errors [Array<String>] Error accumulator array
129
+ # @return [void]
130
+ def validate_machine_name(errors)
131
+ return if @machine_name.nil?
132
+
133
+ # Convert to string for regex matching (defensive programming)
134
+ machine_name_str = @machine_name.to_s
135
+ errors << MACHINE_NAME_FORMAT_ERROR unless machine_name_str.match?(MACHINE_NAME_PATTERN)
136
+ end
137
+
138
+ # Validate that ssh_username attribute is not empty if set.
139
+ #
140
+ # @param errors [Array<String>] Error accumulator array
141
+ # @return [void]
142
+ def validate_ssh_username(errors)
143
+ return if @ssh_username.nil?
144
+ return unless @ssh_username.to_s.strip.empty?
145
+
146
+ errors << SSH_USERNAME_EMPTY_ERROR
147
+ end
148
+
149
+ # Validate that forward_agent is a boolean if set.
150
+ #
151
+ # @param errors [Array<String>] Error accumulator array
152
+ # @return [void]
153
+ def validate_forward_agent(errors)
154
+ return if @forward_agent.nil?
155
+ return if [true, false].include?(@forward_agent)
156
+
157
+ errors << FORWARD_AGENT_BOOLEAN_ERROR
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module OrbStack
5
+ # Base error class for OrbStack provider errors.
6
+ # Inherits from Vagrant's VagrantError to integrate with Vagrant's error handling.
7
+ class Errors < Vagrant::Errors::VagrantError
8
+ error_namespace('vagrant_orbstack.errors')
9
+ end
10
+
11
+ # Error raised when OrbStack is not installed.
12
+ class OrbStackNotInstalled < Errors
13
+ error_key(:orbstack_not_installed)
14
+ end
15
+
16
+ # Error raised when OrbStack is not running.
17
+ class OrbStackNotRunning < Errors
18
+ error_key(:orbstack_not_running)
19
+ end
20
+
21
+ # Error raised when an OrbStack CLI command fails.
22
+ class CommandExecutionError < Errors
23
+ error_key(:command_execution_error)
24
+ end
25
+
26
+ # Error raised when an OrbStack CLI command times out.
27
+ class CommandTimeoutError < Errors
28
+ error_key(:command_timeout_error)
29
+ end
30
+
31
+ # Error raised when machine name collision cannot be resolved after retries.
32
+ class MachineNameCollisionError < Errors
33
+ error_key(:machine_name_collision)
34
+ end
35
+
36
+ # Error raised when SSH is not ready on a machine.
37
+ class SSHNotReady < Errors
38
+ error_key(:ssh_not_ready)
39
+ end
40
+
41
+ # Error raised when SSH connection fails.
42
+ class SSHConnectionFailed < Errors
43
+ error_key(:ssh_connection_failed)
44
+ end
45
+
46
+ # Reopen Errors class to add nested constants for namespaced access
47
+ # This allows both VagrantPlugins::OrbStack::CommandTimeoutError
48
+ # and VagrantPlugins::OrbStack::Errors::CommandTimeoutError to work
49
+ class Errors
50
+ # Alias error classes inside Errors namespace for tests that expect them there
51
+ OrbStackNotInstalled = ::VagrantPlugins::OrbStack::OrbStackNotInstalled
52
+ OrbStackNotInstalledError = OrbStackNotInstalled
53
+ OrbStackNotRunning = ::VagrantPlugins::OrbStack::OrbStackNotRunning
54
+ CommandExecutionError = ::VagrantPlugins::OrbStack::CommandExecutionError
55
+ CommandTimeoutError = ::VagrantPlugins::OrbStack::CommandTimeoutError
56
+ MachineNameCollisionError = ::VagrantPlugins::OrbStack::MachineNameCollisionError
57
+ SSHNotReady = ::VagrantPlugins::OrbStack::SSHNotReady
58
+ SSHConnectionFailed = ::VagrantPlugins::OrbStack::SSHConnectionFailed
59
+
60
+ # Alias for CLI errors
61
+ OrbStackCLIError = CommandExecutionError
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vagrant-orbstack/version'
4
+
5
+ module VagrantPlugins
6
+ # OrbStack provider plugin namespace
7
+ module OrbStack
8
+ # OrbStack provider plugin for Vagrant.
9
+ #
10
+ # This plugin enables OrbStack as a provider backend for Vagrant,
11
+ # allowing users to create and manage Linux development environments
12
+ # on macOS using OrbStack's high-performance virtualization.
13
+ #
14
+ # @api public
15
+ class Plugin < Vagrant.plugin('2')
16
+ name 'vagrant-orbstack'
17
+ description 'Enables OrbStack as a Vagrant provider for macOS development'
18
+
19
+ # Register OrbStack provider with Vagrant.
20
+ #
21
+ # @api private
22
+ provider(:orbstack, priority: 5) do
23
+ require_relative 'provider'
24
+ Provider
25
+ end
26
+
27
+ # Register configuration class for OrbStack provider.
28
+ #
29
+ # @api private
30
+ config(:orbstack, :provider) do
31
+ require_relative 'config'
32
+ Config
33
+ end
34
+
35
+ # Setup I18n locale files for error messages
36
+ def self.setup!
37
+ locale_path = Pathname.new(File.expand_path('../locales', __dir__))
38
+ I18n.load_path << locale_path.join('en.yml') if locale_path.exist?
39
+ end
40
+ end
41
+
42
+ # Initialize I18n on plugin load
43
+ Plugin.setup!
44
+ end
45
+ end
@@ -0,0 +1,387 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'vagrant-orbstack/errors'
6
+ require 'vagrant-orbstack/util/state_cache'
7
+ require 'vagrant-orbstack/util/orbstack_cli'
8
+ require 'vagrant-orbstack/action'
9
+
10
+ module VagrantPlugins
11
+ module OrbStack
12
+ # Vagrant provider implementation for OrbStack.
13
+ #
14
+ # This class implements the Vagrant provider interface, delegating
15
+ # machine lifecycle operations to OrbStack via CLI commands.
16
+ #
17
+ # @api public
18
+ class Provider < Vagrant.plugin('2', :provider)
19
+ # Initialize the provider with a machine instance.
20
+ #
21
+ # @param [Vagrant::Machine] machine The machine this provider is for
22
+ # @api public
23
+ def initialize(machine)
24
+ @machine = machine
25
+ @logger = Log4r::Logger.new('vagrant_orbstack::provider')
26
+ end
27
+
28
+ # Return action middleware for requested operation.
29
+ #
30
+ # Creates and returns an Action::Builder containing the appropriate
31
+ # middleware stack for the requested operation. Currently only :up
32
+ # is implemented; other actions return nil.
33
+ #
34
+ # @param [Symbol] name The action name (:up, :halt, :destroy, etc.)
35
+ # @return [Vagrant::Action::Builder, nil] Action middleware builder or nil
36
+ # @api public
37
+ # rubocop:disable Metrics/MethodLength
38
+ def action(name)
39
+ case name
40
+ when :up
41
+ Vagrant::Action::Builder.new.tap do |b|
42
+ b.use Action::Create
43
+ end
44
+ when :halt
45
+ Vagrant::Action::Builder.new.tap do |b|
46
+ b.use Action::Halt
47
+ end
48
+ when :reload
49
+ build_reload_action
50
+ when :ssh_run
51
+ Vagrant::Action::Builder.new.tap do |b|
52
+ b.use Action::SSHRun
53
+ end
54
+ when :destroy
55
+ Vagrant::Action::Builder.new.tap do |b|
56
+ b.use Action::Destroy
57
+ end
58
+ # Return nil for unsupported actions (future stories, etc.)
59
+ end
60
+ end
61
+ # rubocop:enable Metrics/MethodLength
62
+
63
+ # Provide SSH connection information for the machine.
64
+ #
65
+ # Returns SSH connection parameters for Vagrant to connect to the machine
66
+ # using OrbStack's SSH proxy architecture.
67
+ #
68
+ # CRITICAL: OrbStack uses SSH proxy at localhost:32222, NOT direct SSH to VM IP.
69
+ #
70
+ # Returns nil if the machine is not running.
71
+ #
72
+ # @return [Hash, nil] SSH connection parameters with keys:
73
+ # - :host - Always '127.0.0.1' (OrbStack SSH proxy, NOT VM IP)
74
+ # - :port - Always 32222 (OrbStack SSH proxy port, NOT 22)
75
+ # - :username - Machine ID for proxy routing
76
+ # - :private_key_path - OrbStack's auto-generated ED25519 key
77
+ # - :forward_agent - Whether to forward SSH agent (from config)
78
+ # @api public
79
+ def ssh_info
80
+ # Return nil if machine is not running
81
+ current_state = state
82
+ return nil if %i[not_created stopped].include?(current_state.id)
83
+
84
+ # Return OrbStack SSH proxy configuration
85
+ {
86
+ host: '127.0.0.1',
87
+ port: 32_222,
88
+ username: @machine.id,
89
+ private_key_path: File.expand_path('~/.orbstack/ssh/id_ed25519'),
90
+ proxy_command: orbstack_proxy_command,
91
+ forward_agent: @machine.provider_config.forward_agent
92
+ }
93
+ end
94
+
95
+ # Return current machine state.
96
+ #
97
+ # Queries OrbStack CLI to determine the current state of the machine.
98
+ # Results are cached with a 5-second TTL to reduce redundant CLI calls.
99
+ # State is automatically invalidated when state-changing actions occur.
100
+ #
101
+ # @return [Vagrant::MachineState] Current state of the machine
102
+ # @api public
103
+ def state
104
+ # Return early if machine ID is nil
105
+ return not_created_state('The machine has not been created') if @machine.id.nil?
106
+
107
+ # Check cache first
108
+ cached_state = state_cache.get(@machine.id)
109
+ return cached_state if cached_state
110
+
111
+ # Cache miss: Query OrbStack CLI
112
+ query_and_cache_state
113
+ rescue StandardError => e
114
+ # Handle query errors gracefully
115
+ handle_state_query_error(e)
116
+ end
117
+
118
+ # Invalidate the state cache.
119
+ #
120
+ # Clears all cached state entries, forcing the next state query to
121
+ # fetch fresh data from OrbStack CLI. This is typically called by
122
+ # action middleware after state-changing operations (create, start, stop).
123
+ #
124
+ # @return [void]
125
+ # @api public
126
+ def invalidate_state_cache
127
+ state_cache.invalidate_all
128
+ end
129
+
130
+ # Human-readable provider description.
131
+ #
132
+ # @return [String] Provider name
133
+ # @api public
134
+ def to_s
135
+ 'OrbStack'
136
+ end
137
+
138
+ # Callback invoked when the machine ID changes.
139
+ #
140
+ # Persists the new machine ID to the data directory for retrieval
141
+ # in future Vagrant sessions. This is called by Vagrant core when
142
+ # a machine is created or its ID is updated.
143
+ #
144
+ # The guard clause ensures we only persist when the machine has a valid ID,
145
+ # as some test scenarios may not have @machine.id available.
146
+ #
147
+ # @return [void]
148
+ # @api public
149
+ def machine_id_changed
150
+ # Guard clause: Only persist if machine has an ID
151
+ # Some test scenarios may not have @machine.id available
152
+ return unless @machine.respond_to?(:id) && !@machine.id.nil?
153
+
154
+ write_machine_id(@machine.id)
155
+ end
156
+
157
+ # Read the machine ID from persistent storage.
158
+ #
159
+ # Reads the machine ID from the id file in the data directory.
160
+ # Returns nil if the file doesn't exist or cannot be read.
161
+ #
162
+ # @return [String, nil] The machine ID if found, nil otherwise
163
+ # @raise [Errno::EACCES] If permission denied (logged and returns nil)
164
+ # @raise [Errno::ENOENT] If file not found (logged and returns nil)
165
+ # @raise [Encoding::InvalidByteSequenceError] If file contains invalid data (logged and returns nil)
166
+ # @api public
167
+ def read_machine_id
168
+ return nil unless File.exist?(id_file_path)
169
+
170
+ File.read(id_file_path).strip
171
+ rescue Errno::EACCES, Errno::ENOENT, Encoding::InvalidByteSequenceError => e
172
+ # Log error and return nil - graceful degradation for non-critical errors
173
+ @machine.ui&.warn("OrbStack: Could not read machine ID: #{e.message}")
174
+ nil
175
+ end
176
+
177
+ # Write the machine ID to persistent storage.
178
+ #
179
+ # Writes the machine ID to the id file in the data directory.
180
+ # Creates the directory if it doesn't exist.
181
+ #
182
+ # @param [String] machine_id The machine ID to persist
183
+ # @return [void]
184
+ # @raise [Errno::EACCES] If permission denied
185
+ # @raise [Errno::ENOSPC] If disk is full
186
+ # @raise [Errno::EROFS] If filesystem is read-only
187
+ # @api public
188
+ def write_machine_id(machine_id)
189
+ ensure_data_dir_exists
190
+ File.write(id_file_path, machine_id)
191
+ rescue Errno::EACCES, Errno::ENOSPC, Errno::EROFS => e
192
+ # Log error and re-raise - critical errors that cannot be ignored
193
+ @machine.ui&.error("OrbStack: Could not write machine ID: #{e.message}")
194
+ raise
195
+ end
196
+
197
+ # Read machine metadata from persistent storage.
198
+ #
199
+ # Reads metadata from the metadata.json file in the data directory.
200
+ # Returns an empty hash if the file doesn't exist or contains invalid JSON.
201
+ #
202
+ # @return [Hash] The metadata hash, or empty hash if not found
203
+ # @raise [JSON::ParserError] If JSON is invalid (logged and returns {})
204
+ # @raise [Errno::EACCES] If permission denied (logged and returns {})
205
+ # @raise [Errno::ENOENT] If file not found (logged and returns {})
206
+ # @raise [Encoding::InvalidByteSequenceError] If file contains invalid data (logged and returns {})
207
+ # @api public
208
+ def read_metadata
209
+ return {} unless File.exist?(metadata_file_path)
210
+
211
+ JSON.parse(File.read(metadata_file_path))
212
+ rescue JSON::ParserError, Errno::EACCES, Errno::ENOENT, Encoding::InvalidByteSequenceError => e
213
+ # Log error and return empty hash - graceful degradation for non-critical errors
214
+ @machine.ui&.warn("OrbStack: Could not read metadata: #{e.message}")
215
+ {}
216
+ end
217
+
218
+ # Write machine metadata to persistent storage.
219
+ #
220
+ # Writes metadata to the metadata.json file in the data directory.
221
+ # Creates the directory if it doesn't exist. Formats JSON for readability.
222
+ #
223
+ # @param [Hash] metadata The metadata hash to persist
224
+ # @return [void]
225
+ # @raise [JSON::ParserError] If JSON generation fails
226
+ # @raise [Errno::EACCES] If permission denied
227
+ # @raise [Errno::ENOSPC] If disk is full
228
+ # @raise [Errno::EROFS] If filesystem is read-only
229
+ # @api public
230
+ def write_metadata(metadata)
231
+ ensure_data_dir_exists
232
+ File.write(metadata_file_path, JSON.pretty_generate(metadata))
233
+ rescue JSON::ParserError, Errno::EACCES, Errno::ENOSPC, Errno::EROFS => e
234
+ # Log error and re-raise - critical errors that cannot be ignored
235
+ @machine.ui&.error("OrbStack: Could not write metadata: #{e.message}")
236
+ raise
237
+ end
238
+
239
+ # Path to the machine ID file.
240
+ #
241
+ # @return [Pathname] Path to the ID file
242
+ # @api public
243
+ def id_file_path
244
+ @machine.data_dir.join('id')
245
+ end
246
+
247
+ # Path to the metadata JSON file.
248
+ #
249
+ # @return [Pathname] Path to the metadata file
250
+ # @api public
251
+ def metadata_file_path
252
+ @machine.data_dir.join('metadata.json')
253
+ end
254
+
255
+ private
256
+
257
+ # Get the state cache instance.
258
+ #
259
+ # Lazy-initializes a StateCache instance with 5-second TTL.
260
+ # The cache is shared across all state queries for this provider instance.
261
+ #
262
+ # @return [Util::StateCache] The state cache instance
263
+ # @api private
264
+ def state_cache
265
+ @state_cache ||= Util::StateCache.new(ttl: 5)
266
+ end
267
+
268
+ # Generate OrbStack SSH ProxyCommand.
269
+ #
270
+ # OrbStack routes all SSH connections through the OrbStack Helper app
271
+ # using a ProxyCommand. This returns the correctly formatted command
272
+ # that Vagrant's SSH layer will use.
273
+ #
274
+ # @return [String] ProxyCommand string for SSH config
275
+ # @api private
276
+ def orbstack_proxy_command
277
+ helper_path = '/Applications/OrbStack.app/Contents/Frameworks/' \
278
+ 'OrbStack Helper.app/Contents/MacOS/OrbStack Helper'
279
+ "'#{helper_path}' ssh-proxy-fdpass #{Process.uid}"
280
+ end
281
+
282
+ # Map OrbStack machine info to Vagrant state tuple.
283
+ #
284
+ # Converts OrbStack machine status to Vagrant state representation.
285
+ # Returns a tuple of [state_id, short_description, long_description].
286
+ #
287
+ # @param machine_info [Hash, nil] Machine info from OrbStack CLI with :name and :status,
288
+ # or nil if machine was not found
289
+ # @return [Array<Symbol, String, String>] Tuple of [state_id, short_desc, long_desc]
290
+ # @api private
291
+ def map_orbstack_state_to_vagrant(machine_info)
292
+ if machine_info.nil?
293
+ [:not_created, 'not created', 'Machine does not exist in OrbStack']
294
+ elsif machine_info[:status] == 'running'
295
+ [:running, 'running', 'Machine is running in OrbStack']
296
+ elsif machine_info[:status] == 'stopped'
297
+ [:stopped, 'stopped', 'Machine is stopped']
298
+ else
299
+ # Unknown state - treat as not created
300
+ [:not_created, 'unknown', 'Machine state is unknown']
301
+ end
302
+ end
303
+
304
+ # Create a :not_created MachineState with appropriate description.
305
+ #
306
+ # @param reason [String] The reason the machine is not created
307
+ # @return [Vagrant::MachineState] A not_created state object
308
+ # @api private
309
+ def not_created_state(reason)
310
+ Vagrant::MachineState.new(:not_created, 'not created', reason)
311
+ end
312
+
313
+ # Query OrbStack CLI for current machine state and cache result.
314
+ #
315
+ # @return [Vagrant::MachineState] The queried machine state
316
+ # @api private
317
+ def query_and_cache_state
318
+ machines = Util::OrbStackCLI.list_machines
319
+ our_machine = machines.find { |m| m[:name] == @machine.id }
320
+
321
+ # Map OrbStack state to Vagrant state
322
+ state_id, short_desc, long_desc = map_orbstack_state_to_vagrant(our_machine)
323
+
324
+ # Create MachineState and cache it
325
+ machine_state = Vagrant::MachineState.new(state_id, short_desc, long_desc)
326
+ state_cache.set(@machine.id, machine_state)
327
+
328
+ machine_state
329
+ end
330
+
331
+ # Handle error during state query and return not_created state.
332
+ #
333
+ # @param error [Exception] The error that occurred
334
+ # @return [Vagrant::MachineState] A not_created state object
335
+ # @api private
336
+ def handle_state_query_error(error)
337
+ if error.is_a?(CommandTimeoutError)
338
+ @machine.ui.warn('OrbStack: Command timeout querying machine state')
339
+ @logger.warn("State query timed out: #{error.message}")
340
+ not_created_state('Timeout querying machine state')
341
+ else
342
+ @machine.ui.warn("OrbStack: Error querying machine state: #{error.message}")
343
+ @logger.error("Failed to query machine state: #{error.message}")
344
+ not_created_state('Error querying machine state')
345
+ end
346
+ end
347
+
348
+ # Ensure the data directory exists.
349
+ #
350
+ # Creates the data directory if it doesn't exist.
351
+ #
352
+ # @return [void]
353
+ # @api private
354
+ def ensure_data_dir_exists
355
+ dir = @machine.data_dir
356
+ FileUtils.mkdir_p(dir)
357
+ end
358
+
359
+ # Build reload action: Halt → Start → (optional) Provision
360
+ #
361
+ # @param env [Hash] Environment hash (unused, for consistency)
362
+ # @return [Vagrant::Action::Builder] Configured action builder
363
+ # @api private
364
+ def build_reload_action(_env = nil)
365
+ Vagrant::Action::Builder.new.tap do |b|
366
+ b.use Action::Halt
367
+ b.use Action::Start
368
+ include_provisioning(b)
369
+ end
370
+ end
371
+
372
+ # Include provisioning middleware if available.
373
+ #
374
+ # Conditionally includes Vagrant's built-in Provision middleware.
375
+ # Defensive check ensures compatibility if middleware isn't available.
376
+ #
377
+ # @param builder [Vagrant::Action::Builder] Builder to modify
378
+ # @return [void]
379
+ # @api private
380
+ def include_provisioning(builder)
381
+ return unless defined?(Vagrant::Action::Builtin::Provision)
382
+
383
+ builder.use Vagrant::Action::Builtin::Provision
384
+ end
385
+ end
386
+ end
387
+ end