Vagrant: From Versioned Inputs to Typed Observations
Vagrant is the main walkthrough because it combines a versioned command surface, a machine-readable wire protocol, human UI templates and meaningful machine observations. The integration exposes VM and communicator observations while keeping generated commands, protocol decoding and terminal status separate.
Its rich event catalogue is not a universal state model for every provider and plugin. The study follows the evidence behind the implemented records.
Acquisition and help replay
VagrantVersionCollector and VagrantImagePlanResolver provide version discovery and image recipes. The Design pipeline uses UseVersionImage, UseContainer and a logging-wrapped VagrantHelpParser with -h.
The replay pipeline replaces preparation with UseCachedHelp. The parser and scraper stages remain the same, producing vagrant-<version>.json. VagrantEnvironmentCatalog.ReadProcessEnvironment supplies declared variables to the acquisition runner.
This versioned help establishes command construction. It does not establish the messages printed while a VM boots.
Generated API and assembly ownership
Vagrant.Contracts requests command generation only. The implementation requests builders and client generation. Both supply the help files, while Contracts also supplies the environment catalogue to the Design.Lib analyzer target.
VagrantUpCommand, VagrantEnvironment, VagrantEvent, result records and IVagrantRunner are contract types. VagrantRunner, strategies, parsers and VagrantUpCollector belong to the implementation.
The builder/client is a convenience for construction. Enumerating the runner starts execution. The contracts chapter shows business code without either the concrete runner or builder dependency.
A complete offline command
This example builds a command through the generated client, then uses the actual Vagrant runner and parsers with synthetic output. It starts no binary.
using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.BinaryWrapper.Testing;
using FrenchExDev.Net.Vagrant;
var binding = new BinaryBinding
{
Identifier = new BinaryIdentifier("vagrant"),
ExecutablePath = "unused",
DetectedVersion = SemanticVersion.Parse("2.4.9"),
};
var command = await new VagrantClient(binding).UpAsync(builder => builder
.WithMachineReadable(true)
.WithEnvironment(new VagrantEnvironment { Home = "example/.vagrant.d" }));
var transport = new FakeProcessRunner([
new OutputLine("1700000000,web,state,running", OutputSource.StdOut),
new OutputLine(
"1700000001,web,ui,output,Machine booted and ready!",
OutputSource.StdOut),
]);
IVagrantRunner runner = new VagrantRunner(
new CommandExecutor(new DictionaryBinaryResolver(binding), transport));
await foreach (var evt in runner.StreamAsync(command))
{
if (evt is VagrantMachineStateObserved state)
Console.WriteLine($"Provider state: {state.State}");
if (evt is VagrantMachineBooted booted)
Console.WriteLine($"Communicator ready: {booted.MachineName}");
if (evt.OutputLine is { } original)
Console.WriteLine($"{original.Source}: {original.Text}");
if (evt is VagrantCommandExited exited)
Console.WriteLine($"Actual exit: {exited.ExitCode}");
}using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.BinaryWrapper.Testing;
using FrenchExDev.Net.Vagrant;
var binding = new BinaryBinding
{
Identifier = new BinaryIdentifier("vagrant"),
ExecutablePath = "unused",
DetectedVersion = SemanticVersion.Parse("2.4.9"),
};
var command = await new VagrantClient(binding).UpAsync(builder => builder
.WithMachineReadable(true)
.WithEnvironment(new VagrantEnvironment { Home = "example/.vagrant.d" }));
var transport = new FakeProcessRunner([
new OutputLine("1700000000,web,state,running", OutputSource.StdOut),
new OutputLine(
"1700000001,web,ui,output,Machine booted and ready!",
OutputSource.StdOut),
]);
IVagrantRunner runner = new VagrantRunner(
new CommandExecutor(new DictionaryBinaryResolver(binding), transport));
await foreach (var evt in runner.StreamAsync(command))
{
if (evt is VagrantMachineStateObserved state)
Console.WriteLine($"Provider state: {state.State}");
if (evt is VagrantMachineBooted booted)
Console.WriteLine($"Communicator ready: {booted.MachineName}");
if (evt.OutputLine is { } original)
Console.WriteLine($"{original.Source}: {original.Text}");
if (evt is VagrantCommandExited exited)
Console.WriteLine($"Actual exit: {exited.ExitCode}");
}The provider-state line and readiness line produce different records. The final exit has no source line. Replacing the transport with a real process runner would perform a VM operation and requires an intended Vagrant project and provider.
Machine records and semantic projection
The reviewed protocol carries a timestamp, target, type and data fields. Known kinds include action, metadata, provider/state observations, machine identity, forwarded ports, communicator configuration, box/plugin attributes and error-exit.
Wire evidence is retained before projections. A forwarded-port record can validate numeric ports. A plugin-version target identifies a plugin, while a machine-state target identifies a machine; the parser does not pretend all target strings have the same entity meaning.
An action end means the provider action returned. It does not prove that the surrounding process exited or that provisioning changed the machine's state. error-exit carries a Ruby exception class and message, not the operating system's numeric exit code.
Readiness comes from a reviewed UI shape
The source inventory pins Vagrant 2.4.9 at 97d5ea2501e81d05eacac7fa7be6fc6665c10b74. The boot observation is based on the machine-readable UI envelope and the reviewed English communicator-ready payload.
There is no dedicated boot-completed structured record in that inspected source. Recognition therefore checks the emitting UI method, complete message and consistent machine target. A state of running, a translated payload, a wrong target prefix or an added field does not become boot readiness.
Already-running paths and providers that bypass this step can complete successfully without a boot observation. Absence of the event is not universal proof that a machine is unusable.
The maintenance-generated UI catalogue
The developer tool inventories upstream translations and suitable Ruby literals. It produces checked-in event records, named parameter models, a matching catalogue, source inventory and examples.
Its reviewed extraction is lexical, not a Ruby interpreter. Known dynamic key/method selections are bound explicitly. External scripts, heredocs, arbitrary expressions and third-party plugins remain generic UI output.
Template matching preserves original messages, checks repeated parameters and uses specificity rules. Ambiguity is represented explicitly. UI interpolation parameters remain strings; use structured protocol values when validated numbers are required.
Ordinary builds compile these files. They do not rerun catalogue extraction. Commands and environments, by contrast, are generated by Roslyn during compilation.
Environment semantics
Home, WorkingDirectory, DefaultProvider and related properties configure the Vagrant process. A catalogue baseline of 2.4.0 is separate from the output-profile baseline of 2.4.9.
NoPlugins is presence-based: false removes an inherited VAGRANT_NO_PLUGINS. BoxUpdateCheckDisable is non-empty-based: Disabled emits an empty value. Neither should be interpreted through a single generic boolean encoding.
WorkingDirectory maps to VAGRANT_CWD; it is a Vagrant configuration input, not a general replacement for every process runner's working-directory policy.
Output profiles, results and limits
VagrantOutputStrategies.Default chooses machine-readable parsing when the command requests it, otherwise human parsing. Reviewed profiles are confined to [2.4.9, 2.4.10-0). Other versions retain raw output and terminal status.
VagrantUpCollector collects distinct ready machines in observation order, requires terminal zero exit and accounts for error observations. Readiness alone leaves exit unknown. A successful invocation can have an empty readiness list.
The tests exercise wire decoding, channel provenance, target matching, escapes, state versus readiness, unknown versions, fresh parser instances and terminal failure. Catalogue tests verify source-derived models and ambiguous templates. Container integration is a separately enabled check.
Consume readiness as a communicator observation and process exit as an invocation result. Provisioning and service health still need their own evidence.
Source snapshot
Source inspection: 2 October 2026. These implementation paths are relative to Net/FrenchExDev in the local ecosystem checkout. The v2 changes are largely uncommitted; these paths identify inspected code, without claiming that public branch links contain it.
Vagrant/src/FrenchExDev.Net.Vagrant.Design/Program.csVagrant/src/FrenchExDev.Net.Vagrant.Contracts/FrenchExDev.Net.Vagrant.Contracts.csprojVagrant/src/FrenchExDev.Net.Vagrant/FrenchExDev.Net.Vagrant.csprojVagrant/doc/TYPED-OUTPUT.mdVagrant/test/FrenchExDev.Net.Vagrant.Tests